From 78008bc4ecb6e87fbceede3acfa165f062af7282 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 18 Aug 2025 16:07:34 -0700 Subject: [PATCH 01/39] spike: use smart account factory --- packages/demo/backend/package.json | 1 + packages/demo/backend/src/config/env.ts | 4 + packages/demo/backend/src/config/verbs.ts | 1 + .../demo/backend/src/controllers/wallet.ts | 15 +- packages/demo/backend/src/services/lend.ts | 88 ++- .../demo/backend/src/services/wallet.spec.ts | 69 +-- packages/demo/backend/src/services/wallet.ts | 97 +++- .../demo/frontend/src/components/Terminal.tsx | 16 +- packages/sdk/src/abis/smartWallet.ts | 402 +++++++++++++ packages/sdk/src/abis/smartWalletFactory.ts | 76 +++ packages/sdk/src/constants/addresses.ts | 2 + packages/sdk/src/constants/supportedChains.ts | 7 +- packages/sdk/src/index.ts | 2 +- .../providers/morpho/lend.supersim.test.ts | 542 +++++++++--------- packages/sdk/src/services/ChainManager.ts | 44 +- packages/sdk/src/types/service.ts | 4 +- packages/sdk/src/types/verbs.ts | 28 +- packages/sdk/src/types/wallet.ts | 85 +-- packages/sdk/src/verbs.ts | 117 ++-- packages/sdk/src/wallet/index.spec.ts | 326 +++++------ packages/sdk/src/wallet/index.ts | 172 +++--- packages/sdk/src/wallet/providers/privy.ts | 390 ++++++------- packages/sdk/src/wallet/providers/test.ts | 250 ++++---- packages/sdk/src/wallet/wallet.test.ts | 374 ++++++------ pnpm-lock.yaml | 61 ++ 25 files changed, 1935 insertions(+), 1238 deletions(-) create mode 100644 packages/sdk/src/abis/smartWallet.ts create mode 100644 packages/sdk/src/abis/smartWalletFactory.ts create mode 100644 packages/sdk/src/constants/addresses.ts diff --git a/packages/demo/backend/package.json b/packages/demo/backend/package.json index b577f3bd..f3d49d18 100644 --- a/packages/demo/backend/package.json +++ b/packages/demo/backend/package.json @@ -39,6 +39,7 @@ "@eth-optimism/verbs-sdk": "workspace:*", "@eth-optimism/viem": "^0.4.13", "@hono/node-server": "^1.14.0", + "@privy-io/server-auth": "^1.31.1", "commander": "^13.1.0", "dotenv": "^16.4.5", "envalid": "^8.1.0", diff --git a/packages/demo/backend/src/config/env.ts b/packages/demo/backend/src/config/env.ts index 2fcc6e6d..34fb2b8e 100644 --- a/packages/demo/backend/src/config/env.ts +++ b/packages/demo/backend/src/config/env.ts @@ -50,6 +50,10 @@ export const env = cleanEnv(process.env, { default: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', }), + PRIVATE_KEY: str({ + default: + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + }), FAUCET_ADDRESS: str({ default: getFaucetAddressDefault(), }), diff --git a/packages/demo/backend/src/config/verbs.ts b/packages/demo/backend/src/config/verbs.ts index 5d48ac76..db959a79 100644 --- a/packages/demo/backend/src/config/verbs.ts +++ b/packages/demo/backend/src/config/verbs.ts @@ -25,6 +25,7 @@ export function createVerbsConfig(): VerbsConfig { rpcUrl: env.RPC_URL, }, ], + privateKey: env.PRIVATE_KEY as `0x${string}`, } } diff --git a/packages/demo/backend/src/controllers/wallet.ts b/packages/demo/backend/src/controllers/wallet.ts index 33cdd6c9..1a2f9dc4 100644 --- a/packages/demo/backend/src/controllers/wallet.ts +++ b/packages/demo/backend/src/controllers/wallet.ts @@ -58,13 +58,16 @@ export class WalletController { const { params: { userId }, } = validation.data - const wallet = await walletService.createWallet(userId) + const { privyAddress, smartWalletAddress } = + await walletService.createWallet() return c.json({ - address: wallet.address, + privyAddress, + smartWalletAddress, userId, } satisfies CreateWalletResponse) } catch (error) { + console.error(error) return c.json( { error: 'Failed to create wallet', @@ -86,7 +89,7 @@ export class WalletController { const { params: { userId }, } = validation.data - const wallet = await walletService.getWallet(userId) + const { wallet } = await walletService.getWallet(userId) if (!wallet) { return c.json( @@ -127,9 +130,9 @@ export class WalletController { const wallets = await walletService.getAllWallets({ limit, cursor }) return c.json({ - wallets: wallets.map((wallet) => ({ - address: wallet.address, - id: wallet.id, + wallets: wallets.map(({ privyWallet }) => ({ + address: privyWallet.address as Address, + id: privyWallet.id, })), count: wallets.length, } satisfies GetAllWalletsResponse) diff --git a/packages/demo/backend/src/services/lend.ts b/packages/demo/backend/src/services/lend.ts index ef6d3788..23096910 100644 --- a/packages/demo/backend/src/services/lend.ts +++ b/packages/demo/backend/src/services/lend.ts @@ -1,7 +1,15 @@ -import type { LendTransaction, LendVaultInfo } from '@eth-optimism/verbs-sdk' -import type { Address } from 'viem' +import type { + LendTransaction, + LendVaultInfo, + SupportedChainId, + WalletInterface, +} from '@eth-optimism/verbs-sdk' +import { PrivyClient } from '@privy-io/server-auth' +import { env } from 'process' +import type { Address, PublicClient } from 'viem' import { getVerbs } from '../config/verbs.js' +import { getWallet } from './wallet.js' interface VaultBalanceResult { balance: bigint @@ -39,7 +47,7 @@ export async function getVaultBalance( walletId: string, ): Promise { const verbs = getVerbs() - const wallet = await verbs.getWallet(walletId) + const { wallet } = await getWallet(walletId) if (!wallet) { throw new Error(`Wallet not found for user ID: ${walletId}`) @@ -87,8 +95,7 @@ export async function deposit( amount: number, token: string, ): Promise { - const verbs = getVerbs() - const wallet = await verbs.getWallet(walletId) + const { wallet } = await getWallet(walletId) if (!wallet) { throw new Error(`Wallet not found for user ID: ${walletId}`) @@ -102,7 +109,7 @@ export async function executeLendTransaction( lendTransaction: LendTransaction, ): Promise { const verbs = getVerbs() - const wallet = await verbs.getWallet(walletId) + const { wallet } = await getWallet(walletId) if (!wallet) { throw new Error(`Wallet not found for user ID: ${walletId}`) @@ -113,7 +120,9 @@ export async function executeLendTransaction( } const publicClient = verbs.chainManager.getPublicClient(130) - const ethBalance = await publicClient.getBalance({ address: wallet.address }) + const ethBalance = await publicClient.getBalance({ + address: wallet.ownerAddresses[0], + }) const gasEstimate = await estimateGasCost( publicClient, @@ -128,14 +137,16 @@ export async function executeLendTransaction( let depositHash: Address = '0x0' if (lendTransaction.transactionData.approval) { - const approvalSignedTx = await wallet.sign( + const approvalSignedTx = await signOnly( + walletId, lendTransaction.transactionData.approval, ) const approvalHash = await wallet.send(approvalSignedTx, publicClient) await publicClient.waitForTransactionReceipt({ hash: approvalHash }) } - const depositSignedTx = await wallet.sign( + const depositSignedTx = await signOnly( + walletId, lendTransaction.transactionData.deposit, ) depositHash = await wallet.send(depositSignedTx, publicClient) @@ -144,22 +155,52 @@ export async function executeLendTransaction( return { ...lendTransaction, hash: depositHash } } +async function signOnly( + walletId: string, + transactionData: NonNullable['deposit'], +): Promise { + try { + // Get wallet to determine the from address for gas estimation + const { wallet } = await getWallet(walletId) + if (!wallet) { + throw new Error(`Wallet not found: ${walletId}`) + } + const txParams = await wallet.getTxParams(transactionData, 130) + + console.log( + `[PRIVY_PROVIDER] Complete tx params - Type: ${txParams.type}, Nonce: ${txParams.nonce}, Limit: ${txParams.gasLimit}, MaxFee: ${txParams.maxFeePerGas || 'fallback'}, Priority: ${txParams.maxPriorityFeePerGas || 'fallback'}`, + ) + + const privy = new PrivyClient(env.PRIVY_APP_ID!, env.PRIVY_APP_SECRET!) + const response = await privy.walletApi.ethereum.signTransaction({ + walletId, + transaction: txParams, + }) + console.log('Signed transaction', response.signedTransaction) + return response.signedTransaction + } catch (error) { + console.error('Error signing transaction', error) + throw new Error( + `Failed to sign transaction for wallet ${walletId}: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + ) + } +} + async function estimateGasCost( - publicClient: { estimateGas: Function; getGasPrice: Function }, - wallet: { address: Address }, + publicClient: PublicClient, + wallet: WalletInterface, lendTransaction: LendTransaction, ): Promise { let totalGasEstimate = BigInt(0) if (lendTransaction.transactionData?.approval) { try { - const approvalGas = await publicClient.estimateGas({ - account: wallet.address, - to: lendTransaction.transactionData.approval.to, - data: lendTransaction.transactionData.approval.data, - value: BigInt(lendTransaction.transactionData.approval.value), - }) - totalGasEstimate += approvalGas + totalGasEstimate += await wallet.estimateGas( + lendTransaction.transactionData.approval, + publicClient.chain!.id as SupportedChainId, + ) } catch { // Gas estimation failed, continue } @@ -167,13 +208,10 @@ async function estimateGasCost( if (lendTransaction.transactionData?.deposit) { try { - const depositGas = await publicClient.estimateGas({ - account: wallet.address, - to: lendTransaction.transactionData.deposit.to, - data: lendTransaction.transactionData.deposit.data, - value: BigInt(lendTransaction.transactionData.deposit.value), - }) - totalGasEstimate += depositGas + totalGasEstimate += await wallet.estimateGas( + lendTransaction.transactionData.deposit, + publicClient.chain!.id as SupportedChainId, + ) } catch { // Gas estimation failed, continue } diff --git a/packages/demo/backend/src/services/wallet.spec.ts b/packages/demo/backend/src/services/wallet.spec.ts index a1e19a04..b4881d2f 100644 --- a/packages/demo/backend/src/services/wallet.spec.ts +++ b/packages/demo/backend/src/services/wallet.spec.ts @@ -29,19 +29,18 @@ describe('Wallet Service', () => { mockVerbs.createWallet.mockResolvedValue(mockWallet) - const result = await walletService.createWallet(userId) + const result = await walletService.createWallet() expect(mockVerbs.createWallet).toHaveBeenCalledWith(userId) expect(result).toEqual(mockWallet) }) it('should handle wallet creation errors', async () => { - const userId = 'test-user' const error = new Error('Wallet creation failed') mockVerbs.createWallet.mockRejectedValue(error) - await expect(walletService.createWallet(userId)).rejects.toThrow( + await expect(walletService.createWallet()).rejects.toThrow( 'Wallet creation failed', ) }) @@ -143,70 +142,6 @@ describe('Wallet Service', () => { }) }) - describe('getOrCreateWallet', () => { - it('should return existing wallet if found', async () => { - const userId = 'test-user' - const existingWallet = { - id: 'wallet-123', - address: '0x1234567890123456789012345678901234567890', - } - - mockVerbs.getWallet.mockResolvedValue(existingWallet) - - const result = await walletService.getOrCreateWallet(userId) - - expect(mockVerbs.getWallet).toHaveBeenCalledWith(userId) - expect(mockVerbs.createWallet).not.toHaveBeenCalled() - expect(result).toEqual(existingWallet) - }) - - it('should create new wallet if not found', async () => { - const userId = 'new-user' - const newWallet = { - id: 'wallet-456', - address: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', - } - - mockVerbs.getWallet.mockResolvedValue(null) - mockVerbs.createWallet.mockResolvedValue(newWallet) - - const result = await walletService.getOrCreateWallet(userId) - - expect(mockVerbs.getWallet).toHaveBeenCalledWith(userId) - expect(mockVerbs.createWallet).toHaveBeenCalledWith(userId) - expect(result).toEqual(newWallet) - }) - - it('should handle creation failure after wallet not found', async () => { - const userId = 'new-user' - const createError = new Error('Wallet creation failed') - - mockVerbs.getWallet.mockResolvedValue(null) - mockVerbs.createWallet.mockRejectedValue(createError) - - await expect(walletService.getOrCreateWallet(userId)).rejects.toThrow( - 'Wallet creation failed', - ) - - expect(mockVerbs.getWallet).toHaveBeenCalledWith(userId) - expect(mockVerbs.createWallet).toHaveBeenCalledWith(userId) - }) - - it('should handle get wallet failure', async () => { - const userId = 'test-user' - const getError = new Error('Failed to get wallet') - - mockVerbs.getWallet.mockRejectedValue(getError) - - await expect(walletService.getOrCreateWallet(userId)).rejects.toThrow( - 'Failed to get wallet', - ) - - expect(mockVerbs.getWallet).toHaveBeenCalledWith(userId) - expect(mockVerbs.createWallet).not.toHaveBeenCalled() - }) - }) - describe('getBalance', () => { it('should return balance when wallet exists', async () => { const userId = 'test-user' diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index 57bdf3a1..3d47842f 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -5,12 +5,17 @@ import type { WalletInterface, } from '@eth-optimism/verbs-sdk' import { unichain } from '@eth-optimism/viem/chains' +import { + PrivyClient, + type WalletApiWalletResponseType, +} from '@privy-io/server-auth' import type { Address, Hex } from 'viem' import { createPublicClient, createWalletClient, formatEther, formatUnits, + getAddress, http, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' @@ -21,43 +26,71 @@ import { env } from '@/config/env.js' import { getVerbs } from '../config/verbs.js' -export async function createWallet(userId: string): Promise { +export async function createWallet(): Promise<{ + privyAddress: string + smartWalletAddress: string +}> { + /** + * + */ + const privy = new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET) + const wallet = await privy.walletApi.createWallet({ + chainType: 'ethereum', + }) const verbs = getVerbs() - return await verbs.createWallet(userId) + const addresses = await verbs.createWallet([getAddress(wallet.address)]) + return { privyAddress: wallet.address, smartWalletAddress: addresses[0].address } } -export async function getWallet( - userId: string, -): Promise { +export async function getWallet(userId: string): Promise<{ + privyWallet: WalletApiWalletResponseType + wallet: WalletInterface +}> { + const privy = new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET) + const privyWallet = await privy.walletApi.getWallet({ id: userId }) + if (!privyWallet) { + throw new Error('Wallet not found') + } const verbs = getVerbs() - return await verbs.getWallet(userId) + const wallet = await verbs.getWallet( + [getAddress(privyWallet.address)], + ) + return { privyWallet, wallet } } export async function getAllWallets( options?: GetAllWalletsOptions, -): Promise { - const verbs = getVerbs() - return await verbs.getAllWallets(options) -} +): Promise< + Array<{ privyWallet: WalletApiWalletResponseType; wallet: WalletInterface }> +> { + try { + const privy = new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET) + const response = await privy.walletApi.getWallets({ + limit: options?.limit, + cursor: options?.cursor, + }) -export async function getOrCreateWallet( - userId: string, -): Promise { - let wallet = await getWallet(userId) - if (!wallet) { - wallet = await createWallet(userId) + return Promise.all( + response.data.map((wallet) => { + return getWallet(wallet.id) + }), + ) + } catch { + throw new Error('Failed to retrieve wallets') } - return wallet } export async function getBalance(userId: string): Promise { - const wallet = await getWallet(userId) + const { wallet } = await getWallet(userId) if (!wallet) { throw new Error('Wallet not found') } // Get regular token balances - const tokenBalances = await wallet.getBalance() + const tokenBalances = await wallet.getBalance().catch((error) => { + console.error(error) + throw error + }) // Get vault balances and add them to the response const verbs = getVerbs() @@ -90,7 +123,8 @@ export async function getBalance(userId: string): Promise { } as TokenBalance } return null - } catch { + } catch (error) { + console.error(error) return null } }), @@ -115,12 +149,13 @@ export async function fundWallet( success: boolean tokenType: string to: string + privyAddress: string amount: string }> { // TODO: do this a better way const isLocalSupersim = env.RPC_URL === 'http://127.0.0.1:9545' - const wallet = await getWallet(userId) + const { wallet, privyWallet } = await getWallet(userId) if (!wallet) { throw new Error('Wallet not found') } @@ -145,6 +180,7 @@ Funding is only available in local development with supersim`) }) let dripHash: `0x${string}` + let privyDripHash: `0x${string}` | undefined let amount: bigint let formattedAmount: string @@ -158,6 +194,13 @@ Funding is only available in local development with supersim`) functionName: 'dripETH', args: [wallet.address, amount], }) + privyDripHash = await writeContract(faucetAdminWalletClient, { + account: faucetAdminWalletClient.account, + address: env.FAUCET_ADDRESS as Address, + abi: faucetAbi, + functionName: 'dripETH', + args: [privyWallet.address as `0x${string}`, amount], + }) } else { amount = 1000000000n // 1000 USDC formattedAmount = formatUnits(amount, 6) @@ -171,12 +214,20 @@ Funding is only available in local development with supersim`) }) } - await publicClient.waitForTransactionReceipt({ hash: dripHash }) + await publicClient.waitForTransactionReceipt({ + hash: dripHash, + }) + if (privyDripHash) { + await publicClient.waitForTransactionReceipt({ + hash: privyDripHash, + }) + } return { success: true, tokenType, to: wallet.address, + privyAddress: privyWallet.address, amount: formattedAmount, } } @@ -186,7 +237,7 @@ export async function sendTokens( amount: number, recipientAddress: Address, ): Promise { - const wallet = await getWallet(walletId) + const { wallet } = await getWallet(walletId) if (!wallet) { throw new Error('Wallet not found') } diff --git a/packages/demo/frontend/src/components/Terminal.tsx b/packages/demo/frontend/src/components/Terminal.tsx index 4f15c690..4838c395 100644 --- a/packages/demo/frontend/src/components/Terminal.tsx +++ b/packages/demo/frontend/src/components/Terminal.tsx @@ -6,6 +6,7 @@ import type { } from '@eth-optimism/verbs-sdk' import NavBar from './NavBar' import { verbsApi } from '../api/verbsApi' +import type { Address } from 'viem' interface TerminalLine { id: string @@ -90,7 +91,7 @@ const Terminal = () => { const [screenWidth, setScreenWidth] = useState( typeof window !== 'undefined' ? window.innerWidth : 1200 ) - const [currentWalletList, setCurrentWalletList] = useState(null) + const [currentWalletList, setCurrentWalletList] = useState(null) const inputRef = useRef(null) const terminalRef = useRef(null) @@ -216,7 +217,7 @@ const Terminal = () => { ) if (walletSelectIndex !== -1) { - const formatWalletColumns = (wallets: WalletData[]) => { + const formatWalletColumns = (wallets: GetAllWalletsResponse['wallets']) => { const lines: string[] = [] const totalWallets = wallets.length @@ -545,7 +546,8 @@ Active Wallets: 0`, id: `success-${Date.now()}`, type: 'success', content: `Wallet created successfully! -Address: ${result.address} +Privy Address: ${result.privyAddress} +Smart Wallet Address: ${result.smartWalletAddress} User ID: ${result.userId}`, timestamp: new Date(), } @@ -782,6 +784,7 @@ Tx: https://uniscan.xyz/tx/${result.transaction.hash || 'pending'}`, try { const result = await getAllWallets() + console.log(result) if (result.wallets.length === 0) { const emptyLine: TerminalLine = { @@ -795,9 +798,10 @@ Tx: https://uniscan.xyz/tx/${result.transaction.hash || 'pending'}`, } // Format wallets in responsive columns - const formatWalletColumns = (wallets: WalletData[]) => { + const formatWalletColumns = (wallets: GetAllWalletsResponse['wallets']) => { const lines: string[] = [] const totalWallets = wallets.length + console.log('inside formatWalletColumns', wallets) // Responsive column logic: 1 on mobile, 2 on tablet, 3 on desktop const isMobile = screenWidth < 480 @@ -857,6 +861,7 @@ Tx: https://uniscan.xyz/tx/${result.transaction.hash || 'pending'}`, data: result.wallets, }) } catch (error) { + console.log(error) const errorLine: TerminalLine = { id: `error-${Date.now()}`, type: 'error', @@ -1282,9 +1287,10 @@ ${vaultOptions} setPendingPrompt({ type: 'walletSendSelection', message: '', - data: walletsWithUSDC.map((w) => ({ id: w.id, address: w.address })), + data: walletsWithUSDC.map((w) => ({ id: w.id, address: w.address as Address })), }) } catch (error) { + console.log(error) const errorLine: TerminalLine = { id: `error-${Date.now()}`, type: 'error', diff --git a/packages/sdk/src/abis/smartWallet.ts b/packages/sdk/src/abis/smartWallet.ts new file mode 100644 index 00000000..d592d97c --- /dev/null +++ b/packages/sdk/src/abis/smartWallet.ts @@ -0,0 +1,402 @@ +export const smartWalletAbi = [ + { type: 'constructor', inputs: [], stateMutability: 'nonpayable' }, + { type: 'fallback', stateMutability: 'payable' }, + { type: 'receive', stateMutability: 'payable' }, + { + type: 'function', + name: 'REPLAYABLE_NONCE_KEY', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'addOwnerAddress', + inputs: [{ name: 'owner', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'addOwnerPublicKey', + inputs: [ + { name: 'x', type: 'bytes32', internalType: 'bytes32' }, + { name: 'y', type: 'bytes32', internalType: 'bytes32' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'canSkipChainIdValidation', + inputs: [ + { name: 'functionSelector', type: 'bytes4', internalType: 'bytes4' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'pure', + }, + { + type: 'function', + name: 'domainSeparator', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'eip712Domain', + inputs: [], + outputs: [ + { name: 'fields', type: 'bytes1', internalType: 'bytes1' }, + { name: 'name', type: 'string', internalType: 'string' }, + { name: 'version', type: 'string', internalType: 'string' }, + { name: 'chainId', type: 'uint256', internalType: 'uint256' }, + { name: 'verifyingContract', type: 'address', internalType: 'address' }, + { name: 'salt', type: 'bytes32', internalType: 'bytes32' }, + { name: 'extensions', type: 'uint256[]', internalType: 'uint256[]' }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'entryPoint', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'execute', + inputs: [ + { name: 'target', type: 'address', internalType: 'address' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + { name: 'data', type: 'bytes', internalType: 'bytes' }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'executeBatch', + inputs: [ + { + name: 'calls', + type: 'tuple[]', + internalType: 'struct CoinbaseSmartWallet.Call[]', + components: [ + { name: 'target', type: 'address', internalType: 'address' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + { name: 'data', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'executeWithoutChainIdValidation', + inputs: [{ name: 'calls', type: 'bytes[]', internalType: 'bytes[]' }], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'getUserOpHashWithoutChainId', + inputs: [ + { + name: 'userOp', + type: 'tuple', + internalType: 'struct UserOperation', + components: [ + { name: 'sender', type: 'address', internalType: 'address' }, + { name: 'nonce', type: 'uint256', internalType: 'uint256' }, + { name: 'initCode', type: 'bytes', internalType: 'bytes' }, + { name: 'callData', type: 'bytes', internalType: 'bytes' }, + { name: 'callGasLimit', type: 'uint256', internalType: 'uint256' }, + { + name: 'verificationGasLimit', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'preVerificationGas', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'maxFeePerGas', type: 'uint256', internalType: 'uint256' }, + { + name: 'maxPriorityFeePerGas', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'paymasterAndData', type: 'bytes', internalType: 'bytes' }, + { name: 'signature', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'implementation', + inputs: [], + outputs: [{ name: '$', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'initialize', + inputs: [{ name: 'owners', type: 'bytes[]', internalType: 'bytes[]' }], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'isOwnerAddress', + inputs: [{ name: 'account', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isOwnerBytes', + inputs: [{ name: 'account', type: 'bytes', internalType: 'bytes' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isOwnerPublicKey', + inputs: [ + { name: 'x', type: 'bytes32', internalType: 'bytes32' }, + { name: 'y', type: 'bytes32', internalType: 'bytes32' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isValidSignature', + inputs: [ + { name: 'hash', type: 'bytes32', internalType: 'bytes32' }, + { name: 'signature', type: 'bytes', internalType: 'bytes' }, + ], + outputs: [{ name: 'result', type: 'bytes4', internalType: 'bytes4' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'nextOwnerIndex', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'ownerAtIndex', + inputs: [{ name: 'index', type: 'uint256', internalType: 'uint256' }], + outputs: [{ name: '', type: 'bytes', internalType: 'bytes' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'ownerCount', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'proxiableUUID', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'removeLastOwner', + inputs: [ + { name: 'index', type: 'uint256', internalType: 'uint256' }, + { name: 'owner', type: 'bytes', internalType: 'bytes' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'removeOwnerAtIndex', + inputs: [ + { name: 'index', type: 'uint256', internalType: 'uint256' }, + { name: 'owner', type: 'bytes', internalType: 'bytes' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'removedOwnersCount', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'replaySafeHash', + inputs: [{ name: 'hash', type: 'bytes32', internalType: 'bytes32' }], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'upgradeToAndCall', + inputs: [ + { name: 'newImplementation', type: 'address', internalType: 'address' }, + { name: 'data', type: 'bytes', internalType: 'bytes' }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'validateUserOp', + inputs: [ + { + name: 'userOp', + type: 'tuple', + internalType: 'struct UserOperation', + components: [ + { name: 'sender', type: 'address', internalType: 'address' }, + { name: 'nonce', type: 'uint256', internalType: 'uint256' }, + { name: 'initCode', type: 'bytes', internalType: 'bytes' }, + { name: 'callData', type: 'bytes', internalType: 'bytes' }, + { name: 'callGasLimit', type: 'uint256', internalType: 'uint256' }, + { + name: 'verificationGasLimit', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'preVerificationGas', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'maxFeePerGas', type: 'uint256', internalType: 'uint256' }, + { + name: 'maxPriorityFeePerGas', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'paymasterAndData', type: 'bytes', internalType: 'bytes' }, + { name: 'signature', type: 'bytes', internalType: 'bytes' }, + ], + }, + { name: 'userOpHash', type: 'bytes32', internalType: 'bytes32' }, + { name: 'missingAccountFunds', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [ + { name: 'validationData', type: 'uint256', internalType: 'uint256' }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'AddOwner', + inputs: [ + { + name: 'index', + type: 'uint256', + indexed: true, + internalType: 'uint256', + }, + { name: 'owner', type: 'bytes', indexed: false, internalType: 'bytes' }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RemoveOwner', + inputs: [ + { + name: 'index', + type: 'uint256', + indexed: true, + internalType: 'uint256', + }, + { name: 'owner', type: 'bytes', indexed: false, internalType: 'bytes' }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Upgraded', + inputs: [ + { + name: 'implementation', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'error', + name: 'AlreadyOwner', + inputs: [{ name: 'owner', type: 'bytes', internalType: 'bytes' }], + }, + { type: 'error', name: 'Initialized', inputs: [] }, + { + type: 'error', + name: 'InvalidEthereumAddressOwner', + inputs: [{ name: 'owner', type: 'bytes', internalType: 'bytes' }], + }, + { + type: 'error', + name: 'InvalidImplementation', + inputs: [ + { name: 'implementation', type: 'address', internalType: 'address' }, + ], + }, + { + type: 'error', + name: 'InvalidNonceKey', + inputs: [{ name: 'key', type: 'uint256', internalType: 'uint256' }], + }, + { + type: 'error', + name: 'InvalidOwnerBytesLength', + inputs: [{ name: 'owner', type: 'bytes', internalType: 'bytes' }], + }, + { type: 'error', name: 'LastOwner', inputs: [] }, + { + type: 'error', + name: 'NoOwnerAtIndex', + inputs: [{ name: 'index', type: 'uint256', internalType: 'uint256' }], + }, + { + type: 'error', + name: 'NotLastOwner', + inputs: [ + { name: 'ownersRemaining', type: 'uint256', internalType: 'uint256' }, + ], + }, + { + type: 'error', + name: 'SelectorNotAllowed', + inputs: [{ name: 'selector', type: 'bytes4', internalType: 'bytes4' }], + }, + { type: 'error', name: 'Unauthorized', inputs: [] }, + { type: 'error', name: 'UnauthorizedCallContext', inputs: [] }, + { type: 'error', name: 'UpgradeFailed', inputs: [] }, + { + type: 'error', + name: 'WrongOwnerAtIndex', + inputs: [ + { name: 'index', type: 'uint256', internalType: 'uint256' }, + { name: 'expectedOwner', type: 'bytes', internalType: 'bytes' }, + { name: 'actualOwner', type: 'bytes', internalType: 'bytes' }, + ], + }, +] diff --git a/packages/sdk/src/abis/smartWalletFactory.ts b/packages/sdk/src/abis/smartWalletFactory.ts new file mode 100644 index 00000000..b1b95a28 --- /dev/null +++ b/packages/sdk/src/abis/smartWalletFactory.ts @@ -0,0 +1,76 @@ +export const smartWalletFactoryAbi = [ + { + type: 'constructor', + inputs: [ + { name: 'implementation_', type: 'address', internalType: 'address' }, + ], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'createAccount', + inputs: [ + { name: 'owners', type: 'bytes[]', internalType: 'bytes[]' }, + { name: 'nonce', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [ + { + name: 'account', + type: 'address', + internalType: 'contract CoinbaseSmartWallet', + }, + ], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'getAddress', + inputs: [ + { name: 'owners', type: 'bytes[]', internalType: 'bytes[]' }, + { name: 'nonce', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'implementation', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'initCodeHash', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'event', + name: 'AccountCreated', + inputs: [ + { + name: 'account', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'owners', + type: 'bytes[]', + indexed: false, + internalType: 'bytes[]', + }, + { + name: 'nonce', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'ImplementationUndeployed', inputs: [] }, + { type: 'error', name: 'OwnerRequired', inputs: [] }, +] as const diff --git a/packages/sdk/src/constants/addresses.ts b/packages/sdk/src/constants/addresses.ts new file mode 100644 index 00000000..e6118c1c --- /dev/null +++ b/packages/sdk/src/constants/addresses.ts @@ -0,0 +1,2 @@ +export const smartWalletFactoryAddress = + '0xBA5ED110eFDBa3D005bfC882d75358ACBbB85842' diff --git a/packages/sdk/src/constants/supportedChains.ts b/packages/sdk/src/constants/supportedChains.ts index a0ba0e22..5c40b4d5 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' -export const SUPPORTED_CHAIN_IDS = [mainnet.id, unichain.id, base.id] as const +export const SUPPORTED_CHAIN_IDS = [ + mainnet.id, + unichain.id, + base.id, + 901, +] as const export type SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number] diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 7893a7eb..6495d033 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,3 +1,4 @@ +export type { SupportedChainId } from './constants/supportedChains.js' export { LendProvider, LendProviderMorpho } from './lend/index.js' export { getTokenAddress, SUPPORTED_TOKENS } from './supported/tokens.js' export type { @@ -26,4 +27,3 @@ export type { } from './types/index.js' export { initVerbs, Verbs } from './verbs.js' export { Wallet } from './wallet/index.js' -export { WalletProviderPrivy } from './wallet/providers/privy.js' diff --git a/packages/sdk/src/lend/providers/morpho/lend.supersim.test.ts b/packages/sdk/src/lend/providers/morpho/lend.supersim.test.ts index 52389c6f..b3d784a2 100644 --- a/packages/sdk/src/lend/providers/morpho/lend.supersim.test.ts +++ b/packages/sdk/src/lend/providers/morpho/lend.supersim.test.ts @@ -1,271 +1,271 @@ -import type { ChildProcess } from 'child_process' -import { config } from 'dotenv' -import { type Address, erc20Abi, parseUnits, type PublicClient } from 'viem' -import type { privateKeyToAccount } from 'viem/accounts' -import { unichain } from 'viem/chains' -import { afterAll, beforeAll, describe, expect, it } from 'vitest' - -import type { VerbsInterface } from '../../../types/verbs.js' -import type { Wallet } from '../../../types/wallet.js' -import { - ANVIL_ACCOUNTS, - setupSupersimTest, - stopSupersim, -} from '../../../utils/test.js' -import { initVerbs } from '../../../verbs.js' -import { SUPPORTED_VAULTS } from './vaults.js' - -// Load test environment variables -config({ path: '.env.test.local' }) - -// Use the first supported vault (Gauntlet USDC) -const TEST_VAULT = SUPPORTED_VAULTS[0] -const USDC_ADDRESS = TEST_VAULT.asset.address -const TEST_VAULT_ADDRESS = TEST_VAULT.address -const TEST_WALLET_ID = 'v6c9zr6cjoo91qlopwzo9nhl' -const TEST_WALLET_ADDRESS = - '0x55B05e38597D4365C59A6847f51849B30C381bA2' as Address - -describe('Morpho Lend', () => { - let supersimProcess: ChildProcess - let publicClient: PublicClient - let _testAccount: ReturnType - let verbs: VerbsInterface - let testWallet: Wallet | null - - beforeAll(async () => { - // Set up supersim with funded wallet using helper - const setup = await setupSupersimTest({ - supersim: { - chains: ['unichain'], - l1Port: 8546, - l2StartingPort: 9546, - }, - wallet: { - rpcUrl: 'http://127.0.0.1:9546', - chain: unichain, - amount: '10', - fundUsdc: true, // Request USDC funding for vault testing - usdcAmount: '1000', - // Fund the Privy wallet address - address: TEST_WALLET_ADDRESS, - }, - }) - - supersimProcess = setup.supersimProcess - publicClient = setup.publicClient - _testAccount = setup.testAccount - - // Initialize Verbs SDK with Morpho lending - verbs = initVerbs({ - wallet: { - type: 'privy', - appId: process.env.PRIVY_APP_ID || 'test-app-id', - appSecret: process.env.PRIVY_APP_SECRET || 'test-app-secret', - }, - lend: { - type: 'morpho', - defaultSlippage: 50, - }, - chains: [ - { - chainId: unichain.id, - rpcUrl: 'http://127.0.0.1:9546', - }, - ], - }) - - // Use Privy to get the wallet - const wallet = await verbs.getWallet(TEST_WALLET_ID) - - if (!wallet) { - throw new Error(`Wallet ${TEST_WALLET_ID} not found in Privy`) - } - - testWallet = wallet - - // Verify the address matches what we expect - expect(testWallet!.address.toLowerCase()).toBe( - TEST_WALLET_ADDRESS.toLowerCase(), - ) - }, 60000) - - afterAll(async () => { - await stopSupersim(supersimProcess) - }) - - it('should connect to forked Unichain', async () => { - // Check that we can connect and get the chain ID - const chainId = await publicClient.getChainId() - expect(chainId).toBe(130) // Unichain chain ID - - // Check that our Privy wallet has ETH - const balance = await publicClient.getBalance({ - address: TEST_WALLET_ADDRESS, - }) - expect(balance).toBeGreaterThan(0n) - }) - - it('should execute lend operation with real Morpho transactions', async () => { - // First, verify the vault exists - await verbs.lend.getVault(TEST_VAULT_ADDRESS) - - // Check balances - await publicClient.getBalance({ - address: TEST_WALLET_ADDRESS, - }) - - // Check USDC balance - try { - await publicClient.readContract({ - address: USDC_ADDRESS, - abi: erc20Abi, - functionName: 'balanceOf', - args: [TEST_WALLET_ADDRESS], - }) - } catch { - throw new Error('USDC balance not found') - } - - // Check vault balance before deposit - const vaultBalanceBefore = await verbs.lend.getVaultBalance( - TEST_VAULT_ADDRESS, - TEST_WALLET_ADDRESS, - ) - - // Test the new human-readable API: lend(1, 'usdc') - const lendTx = await testWallet!.lend(1, 'usdc', TEST_VAULT_ADDRESS, { - slippage: 50, // 0.5% - }) - - const expectedAmount = parseUnits('1', 6) // 1 USDC (6 decimals) - - // Validate lend transaction structure - expect(lendTx).toBeDefined() - expect(lendTx.amount).toBe(expectedAmount) - expect(lendTx.asset).toBe(USDC_ADDRESS) - expect(lendTx.marketId).toBe(TEST_VAULT_ADDRESS) - expect(lendTx.apy).toBeGreaterThan(0) - expect(lendTx.slippage).toBe(50) - expect(lendTx.transactionData).toBeDefined() - expect(lendTx.transactionData?.deposit).toBeDefined() - expect(lendTx.transactionData?.approval).toBeDefined() - - // Validate transaction data structure - expect(lendTx.transactionData?.approval?.to).toBe(USDC_ADDRESS) - expect(lendTx.transactionData?.approval?.data).toMatch(/^0x[0-9a-fA-F]+$/) - expect(lendTx.transactionData?.approval?.value).toBe('0x0') - - expect(lendTx.transactionData?.deposit?.to).toBe(TEST_VAULT_ADDRESS) - expect(lendTx.transactionData?.deposit?.data).toMatch(/^0x[0-9a-fA-F]+$/) - expect(lendTx.transactionData?.deposit?.value).toBe('0x0') - - // Get the current nonce for the wallet - await publicClient.getTransactionCount({ - address: TEST_WALLET_ADDRESS, - }) - - // Test signing the approval transaction using wallet.sign() - try { - const approvalTx = lendTx.transactionData!.approval! - - // First, estimate gas for approval transaction on supersim - await publicClient.estimateGas({ - account: TEST_WALLET_ADDRESS, - to: approvalTx.to as `0x${string}`, - data: approvalTx.data as `0x${string}`, - value: BigInt(approvalTx.value), - }) - - const signedApproval = await testWallet!.sign(approvalTx) - expect(signedApproval).toBeDefined() - - // Send the signed transaction to supersim - const approvalTxHash = await testWallet!.send( - signedApproval, - publicClient, - ) - expect(approvalTxHash).toMatch(/^0x[0-9a-fA-F]{64}$/) // Valid tx hash format - - // Wait for approval to be mined - await publicClient.waitForTransactionReceipt({ hash: approvalTxHash }) - } catch { - // This is expected if Privy wallet doesn't have gas on the right network - } - - // Test deposit transaction structure - const depositTx = lendTx.transactionData!.deposit! - - expect(depositTx.to).toBe(TEST_VAULT_ADDRESS) - expect(depositTx.data.length).toBeGreaterThan(10) // Should have encoded function data - expect(depositTx.data.startsWith('0x')).toBe(true) - - // The deposit call data should include the deposit function selector - // deposit(uint256,address) has selector 0x6e553f65 - expect(depositTx.data.startsWith('0x6e553f65')).toBe(true) - - // Test signing the deposit transaction using wallet.sign() - try { - const signedDeposit = await testWallet!.sign(depositTx) - expect(signedDeposit).toBeDefined() - - // Send the signed transaction to supersim - const depositTxHash = await testWallet!.send(signedDeposit, publicClient) - expect(depositTxHash).toMatch(/^0x[0-9a-fA-F]{64}$/) // Valid tx hash format - - // Wait for deposit to be mined - await publicClient.waitForTransactionReceipt({ hash: depositTxHash }) - } catch { - // This is expected if Privy wallet doesn't have gas on the right network - } - - // Check vault balance after deposit attempts - const vaultBalanceAfter = await verbs.lend.getVaultBalance( - TEST_VAULT_ADDRESS, - TEST_WALLET_ADDRESS, - ) - - // For now, we expect the test to fail at signing since Privy needs proper setup - // In production, the balance would increase after successful deposits - expect(vaultBalanceBefore).toBeDefined() - expect(vaultBalanceAfter).toBeDefined() - }, 60000) - - it('should handle different human-readable amounts', async () => { - // Test fractional amounts - const tx1 = await testWallet!.lend(0.5, 'usdc', TEST_VAULT_ADDRESS) - const expectedAmount1 = parseUnits('0.5', 6) // 0.5 USDC - expect(tx1.amount).toBe(expectedAmount1) - - // Test large amounts - const tx2 = await testWallet!.lend(1000, 'usdc', TEST_VAULT_ADDRESS) - const expectedAmount2 = parseUnits('1000', 6) // 1000 USDC - expect(tx2.amount).toBe(expectedAmount2) - - // Test using address instead of symbol - const tx3 = await testWallet!.lend(1, USDC_ADDRESS, TEST_VAULT_ADDRESS) - const expectedAmount3 = parseUnits('1', 6) // 1 USDC - expect(tx3.amount).toBe(expectedAmount3) - expect(tx3.asset).toBe(USDC_ADDRESS) - }, 30000) - - it('should validate input parameters', async () => { - // Test invalid amount - await expect(testWallet!.lend(0, 'usdc')).rejects.toThrow( - 'Amount must be greater than 0', - ) - await expect(testWallet!.lend(-1, 'usdc')).rejects.toThrow( - 'Amount must be greater than 0', - ) - - // Test invalid asset symbol - await expect(testWallet!.lend(1, 'invalid')).rejects.toThrow( - 'Unsupported asset symbol: invalid', - ) - - // Test invalid address format - await expect(testWallet!.lend(1, 'not-an-address')).rejects.toThrow( - 'Unsupported asset symbol', - ) - }, 30000) -}) +// import type { ChildProcess } from 'child_process' +// import { config } from 'dotenv' +// import { type Address, erc20Abi, parseUnits, type PublicClient } from 'viem' +// import type { privateKeyToAccount } from 'viem/accounts' +// import { unichain } from 'viem/chains' +// import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +// import type { VerbsInterface } from '../../../types/verbs.js' +// import type { Wallet } from '../../../types/wallet.js' +// import { +// ANVIL_ACCOUNTS, +// setupSupersimTest, +// stopSupersim, +// } from '../../../utils/test.js' +// import { initVerbs } from '../../../verbs.js' +// import { SUPPORTED_VAULTS } from './vaults.js' + +// // Load test environment variables +// config({ path: '.env.test.local' }) + +// // Use the first supported vault (Gauntlet USDC) +// const TEST_VAULT = SUPPORTED_VAULTS[0] +// const USDC_ADDRESS = TEST_VAULT.asset.address +// const TEST_VAULT_ADDRESS = TEST_VAULT.address +// const TEST_WALLET_ID = 'v6c9zr6cjoo91qlopwzo9nhl' +// const TEST_WALLET_ADDRESS = +// '0x55B05e38597D4365C59A6847f51849B30C381bA2' as Address + +// describe('Morpho Lend', () => { +// let supersimProcess: ChildProcess +// let publicClient: PublicClient +// let _testAccount: ReturnType +// let verbs: VerbsInterface +// let testWallet: Wallet | null + +// beforeAll(async () => { +// // Set up supersim with funded wallet using helper +// const setup = await setupSupersimTest({ +// supersim: { +// chains: ['unichain'], +// l1Port: 8546, +// l2StartingPort: 9546, +// }, +// wallet: { +// rpcUrl: 'http://127.0.0.1:9546', +// chain: unichain, +// amount: '10', +// fundUsdc: true, // Request USDC funding for vault testing +// usdcAmount: '1000', +// // Fund the Privy wallet address +// address: TEST_WALLET_ADDRESS, +// }, +// }) + +// supersimProcess = setup.supersimProcess +// publicClient = setup.publicClient +// _testAccount = setup.testAccount + +// // Initialize Verbs SDK with Morpho lending +// verbs = initVerbs({ +// wallet: { +// type: 'privy', +// appId: process.env.PRIVY_APP_ID || 'test-app-id', +// appSecret: process.env.PRIVY_APP_SECRET || 'test-app-secret', +// }, +// lend: { +// type: 'morpho', +// defaultSlippage: 50, +// }, +// chains: [ +// { +// chainId: unichain.id, +// rpcUrl: 'http://127.0.0.1:9546', +// }, +// ], +// }) + +// // Use Privy to get the wallet +// const wallet = await verbs.getWallet(TEST_WALLET_ID) + +// if (!wallet) { +// throw new Error(`Wallet ${TEST_WALLET_ID} not found in Privy`) +// } + +// testWallet = wallet + +// // Verify the address matches what we expect +// expect(testWallet!.address.toLowerCase()).toBe( +// TEST_WALLET_ADDRESS.toLowerCase(), +// ) +// }, 60000) + +// afterAll(async () => { +// await stopSupersim(supersimProcess) +// }) + +// it('should connect to forked Unichain', async () => { +// // Check that we can connect and get the chain ID +// const chainId = await publicClient.getChainId() +// expect(chainId).toBe(130) // Unichain chain ID + +// // Check that our Privy wallet has ETH +// const balance = await publicClient.getBalance({ +// address: TEST_WALLET_ADDRESS, +// }) +// expect(balance).toBeGreaterThan(0n) +// }) + +// it('should execute lend operation with real Morpho transactions', async () => { +// // First, verify the vault exists +// await verbs.lend.getVault(TEST_VAULT_ADDRESS) + +// // Check balances +// await publicClient.getBalance({ +// address: TEST_WALLET_ADDRESS, +// }) + +// // Check USDC balance +// try { +// await publicClient.readContract({ +// address: USDC_ADDRESS, +// abi: erc20Abi, +// functionName: 'balanceOf', +// args: [TEST_WALLET_ADDRESS], +// }) +// } catch { +// throw new Error('USDC balance not found') +// } + +// // Check vault balance before deposit +// const vaultBalanceBefore = await verbs.lend.getVaultBalance( +// TEST_VAULT_ADDRESS, +// TEST_WALLET_ADDRESS, +// ) + +// // Test the new human-readable API: lend(1, 'usdc') +// const lendTx = await testWallet!.lend(1, 'usdc', TEST_VAULT_ADDRESS, { +// slippage: 50, // 0.5% +// }) + +// const expectedAmount = parseUnits('1', 6) // 1 USDC (6 decimals) + +// // Validate lend transaction structure +// expect(lendTx).toBeDefined() +// expect(lendTx.amount).toBe(expectedAmount) +// expect(lendTx.asset).toBe(USDC_ADDRESS) +// expect(lendTx.marketId).toBe(TEST_VAULT_ADDRESS) +// expect(lendTx.apy).toBeGreaterThan(0) +// expect(lendTx.slippage).toBe(50) +// expect(lendTx.transactionData).toBeDefined() +// expect(lendTx.transactionData?.deposit).toBeDefined() +// expect(lendTx.transactionData?.approval).toBeDefined() + +// // Validate transaction data structure +// expect(lendTx.transactionData?.approval?.to).toBe(USDC_ADDRESS) +// expect(lendTx.transactionData?.approval?.data).toMatch(/^0x[0-9a-fA-F]+$/) +// expect(lendTx.transactionData?.approval?.value).toBe('0x0') + +// expect(lendTx.transactionData?.deposit?.to).toBe(TEST_VAULT_ADDRESS) +// expect(lendTx.transactionData?.deposit?.data).toMatch(/^0x[0-9a-fA-F]+$/) +// expect(lendTx.transactionData?.deposit?.value).toBe('0x0') + +// // Get the current nonce for the wallet +// await publicClient.getTransactionCount({ +// address: TEST_WALLET_ADDRESS, +// }) + +// // Test signing the approval transaction using wallet.sign() +// try { +// const approvalTx = lendTx.transactionData!.approval! + +// // First, estimate gas for approval transaction on supersim +// await publicClient.estimateGas({ +// account: TEST_WALLET_ADDRESS, +// to: approvalTx.to as `0x${string}`, +// data: approvalTx.data as `0x${string}`, +// value: BigInt(approvalTx.value), +// }) + +// const signedApproval = await testWallet!.sign(approvalTx) +// expect(signedApproval).toBeDefined() + +// // Send the signed transaction to supersim +// const approvalTxHash = await testWallet!.send( +// signedApproval, +// publicClient, +// ) +// expect(approvalTxHash).toMatch(/^0x[0-9a-fA-F]{64}$/) // Valid tx hash format + +// // Wait for approval to be mined +// await publicClient.waitForTransactionReceipt({ hash: approvalTxHash }) +// } catch { +// // This is expected if Privy wallet doesn't have gas on the right network +// } + +// // Test deposit transaction structure +// const depositTx = lendTx.transactionData!.deposit! + +// expect(depositTx.to).toBe(TEST_VAULT_ADDRESS) +// expect(depositTx.data.length).toBeGreaterThan(10) // Should have encoded function data +// expect(depositTx.data.startsWith('0x')).toBe(true) + +// // The deposit call data should include the deposit function selector +// // deposit(uint256,address) has selector 0x6e553f65 +// expect(depositTx.data.startsWith('0x6e553f65')).toBe(true) + +// // Test signing the deposit transaction using wallet.sign() +// try { +// const signedDeposit = await testWallet!.sign(depositTx) +// expect(signedDeposit).toBeDefined() + +// // Send the signed transaction to supersim +// const depositTxHash = await testWallet!.send(signedDeposit, publicClient) +// expect(depositTxHash).toMatch(/^0x[0-9a-fA-F]{64}$/) // Valid tx hash format + +// // Wait for deposit to be mined +// await publicClient.waitForTransactionReceipt({ hash: depositTxHash }) +// } catch { +// // This is expected if Privy wallet doesn't have gas on the right network +// } + +// // Check vault balance after deposit attempts +// const vaultBalanceAfter = await verbs.lend.getVaultBalance( +// TEST_VAULT_ADDRESS, +// TEST_WALLET_ADDRESS, +// ) + +// // For now, we expect the test to fail at signing since Privy needs proper setup +// // In production, the balance would increase after successful deposits +// expect(vaultBalanceBefore).toBeDefined() +// expect(vaultBalanceAfter).toBeDefined() +// }, 60000) + +// it('should handle different human-readable amounts', async () => { +// // Test fractional amounts +// const tx1 = await testWallet!.lend(0.5, 'usdc', TEST_VAULT_ADDRESS) +// const expectedAmount1 = parseUnits('0.5', 6) // 0.5 USDC +// expect(tx1.amount).toBe(expectedAmount1) + +// // Test large amounts +// const tx2 = await testWallet!.lend(1000, 'usdc', TEST_VAULT_ADDRESS) +// const expectedAmount2 = parseUnits('1000', 6) // 1000 USDC +// expect(tx2.amount).toBe(expectedAmount2) + +// // Test using address instead of symbol +// const tx3 = await testWallet!.lend(1, USDC_ADDRESS, TEST_VAULT_ADDRESS) +// const expectedAmount3 = parseUnits('1', 6) // 1 USDC +// expect(tx3.amount).toBe(expectedAmount3) +// expect(tx3.asset).toBe(USDC_ADDRESS) +// }, 30000) + +// it('should validate input parameters', async () => { +// // Test invalid amount +// await expect(testWallet!.lend(0, 'usdc')).rejects.toThrow( +// 'Amount must be greater than 0', +// ) +// await expect(testWallet!.lend(-1, 'usdc')).rejects.toThrow( +// 'Amount must be greater than 0', +// ) + +// // Test invalid asset symbol +// await expect(testWallet!.lend(1, 'invalid')).rejects.toThrow( +// 'Unsupported asset symbol: invalid', +// ) + +// // Test invalid address format +// await expect(testWallet!.lend(1, 'not-an-address')).rejects.toThrow( +// 'Unsupported asset symbol', +// ) +// }, 30000) +// }) diff --git a/packages/sdk/src/services/ChainManager.ts b/packages/sdk/src/services/ChainManager.ts index 12016feb..0ffe289b 100644 --- a/packages/sdk/src/services/ChainManager.ts +++ b/packages/sdk/src/services/ChainManager.ts @@ -17,6 +17,32 @@ export class ChainManager { this.publicClients = this.createPublicClients(chains) } + /** + * Get public client for a specific chain + */ + getPublicClient(chainId: (typeof SUPPORTED_CHAIN_IDS)[number]): PublicClient { + const client = this.publicClients.get(chainId) + if (!client) { + throw new Error(`No public client configured for chain ID: ${chainId}`) + } + return client + } + + getRpcUrl(chainId: (typeof SUPPORTED_CHAIN_IDS)[number]): string { + const chainConfig = this.chainConfigs.find((c) => c.chainId === chainId) + if (!chainConfig) { + throw new Error(`No chain config found for chain ID: ${chainId}`) + } + return chainConfig.rpcUrl + } + + /** + * Get supported chain IDs + */ + getSupportedChains() { + return this.chainConfigs.map((c) => c.chainId) + } + /** * Create public clients for all configured chains */ @@ -48,22 +74,4 @@ export class ChainManager { return clients } - - /** - * Get public client for a specific chain - */ - getPublicClient(chainId: (typeof SUPPORTED_CHAIN_IDS)[number]): PublicClient { - const client = this.publicClients.get(chainId) - if (!client) { - throw new Error(`No public client configured for chain ID: ${chainId}`) - } - return client - } - - /** - * Get supported chain IDs - */ - getSupportedChains() { - return this.chainConfigs.map((c) => c.chainId) - } } diff --git a/packages/sdk/src/types/service.ts b/packages/sdk/src/types/service.ts index 7ee1518c..ad8ad907 100644 --- a/packages/sdk/src/types/service.ts +++ b/packages/sdk/src/types/service.ts @@ -30,7 +30,9 @@ export interface GetAllWalletsResponse { */ export interface CreateWalletResponse { /** Wallet address */ - address: Address + privyAddress: string + /** Smart wallet address */ + smartWalletAddress: string /** User ID */ userId: string } diff --git a/packages/sdk/src/types/verbs.ts b/packages/sdk/src/types/verbs.ts index e7788356..c9be83b5 100644 --- a/packages/sdk/src/types/verbs.ts +++ b/packages/sdk/src/types/verbs.ts @@ -1,8 +1,10 @@ +import type { Address, Hash } from 'viem' + import type { ChainManager } from '@/services/ChainManager.js' import type { ChainConfig } from '@/types/chain.js' import type { LendConfig, LendProvider } from './lend.js' -import type { GetAllWalletsOptions, Wallet } from './wallet.js' +import type { Wallet } from './wallet.js' /** * Core Verbs SDK interface @@ -21,22 +23,26 @@ export interface VerbsInterface { readonly chainManager: ChainManager /** * Create a new wallet - * @param userId - User identifier for the wallet + * @param ownerAddresses - User identifier for the wallet * @returns Promise resolving to new wallet instance */ - createWallet(userId: string): Promise - /** - * Get wallet by user ID - * @param userId - User identifier - * @returns Promise resolving to wallet or null if not found - */ - getWallet(userId: string): Promise + createWallet( + ownerAddresses: Address[], + nonce?: bigint, + ): Promise> /** * Get all wallets * @param options - Optional parameters for filtering and pagination * @returns Promise resolving to array of wallets */ - getAllWallets(options?: GetAllWalletsOptions): Promise + // getAllWallets(options?: GetAllWalletsOptions): Promise + /** + * Get the smart wallet address for an owner address + * @param ownerAddress - Owner address + * @param chainId - Chain ID + * @returns Promise resolving to smart wallet address + */ + getWallet(ownerAddresses: Address[], nonce?: bigint): Promise } /** @@ -50,6 +56,8 @@ export interface VerbsConfig { lend?: LendConfig /** Chains to use for the SDK */ chains?: ChainConfig[] + /** Private key for the wallet */ + privateKey?: Hash } /** diff --git a/packages/sdk/src/types/wallet.ts b/packages/sdk/src/types/wallet.ts index d214e81b..f8ea9eb8 100644 --- a/packages/sdk/src/types/wallet.ts +++ b/packages/sdk/src/types/wallet.ts @@ -1,5 +1,6 @@ -import type { Address, Hash } from 'viem' +import type { Address, Hash, Hex, Quantity } from 'viem' +import type { SupportedChainId } from '@/constants/supportedChains.js' import type { LendOptions, LendTransaction, @@ -37,36 +38,17 @@ export interface WalletProvider { * @param transactionData - Transaction data to sign and send * @returns Promise resolving to transaction hash */ - sign(walletId: string, transactionData: TransactionData): Promise } /** * Wallet interface * @description Core wallet interface with blockchain properties and verbs */ -export interface Wallet extends WalletVerbs { - /** Wallet ID */ - id: string +export interface Wallet { /** Wallet address */ address: Address -} - -/** - * Options for getting all wallets - * @description Parameters for filtering and paginating wallet results - */ -export interface GetAllWalletsOptions { - /** Maximum number of wallets to return */ - limit?: number - /** Cursor for pagination */ - cursor?: string -} - -/** - * Wallet verbs/actions - * @description Available actions that can be performed on a wallet - */ -export type WalletVerbs = { + /** Wallet owner addresses */ + ownerAddresses: Address[] /** * Get asset balances aggregated across all supported chains * @returns Promise resolving to array of asset balances @@ -86,18 +68,6 @@ export type WalletVerbs = { marketId?: string, options?: LendOptions, ): Promise - /** - * Sign and send a transaction - * @param transactionData - Transaction data to sign and send - * @returns Promise resolving to transaction hash - */ - signAndSend(transactionData: TransactionData): Promise - /** - * Sign a transaction without sending it - * @param transactionData - Transaction data to sign - * @returns Promise resolving to signed transaction - */ - sign(transactionData: TransactionData): Promise<`0x${string}`> /** * Send a signed transaction * @param signedTransaction - Signed transaction to send @@ -117,4 +87,49 @@ export type WalletVerbs = { asset: AssetIdentifier, recipientAddress: Address, ): Promise + execute(transactionData: TransactionData): Hash + getTxParams( + transactionData: TransactionData, + chainId: SupportedChainId, + ownerIndex?: number, + ): Promise<{ + /** The address the transaction is sent from. Must be hexadecimal formatted. */ + from?: Hex + /** Destination address of the transaction. */ + to?: Hex + /** The nonce to be used for the transaction (hexadecimal or number). */ + nonce?: Quantity + /** (optional) The chain ID of network your transaction will be sent on. */ + chainId?: Quantity + /** (optional) Data to send to the receiving address, especially when calling smart contracts. Must be hexadecimal formatted. */ + data?: Hex + /** (optional) The value (in wei) be sent with the transaction (hexadecimal or number). */ + value?: Quantity + /** (optional) The EIP-2718 transction type (e.g. `2` for EIP-1559 transactions). */ + type?: 0 | 1 | 2 + /** (optional) The max units of gas that can be used by this transaction (hexadecimal or number). */ + gasLimit?: Quantity + /** (optional) The price (in wei) per unit of gas for this transaction (hexadecimal or number), for use in non EIP-1559 transactions (type 0 or 1). */ + gasPrice?: Quantity + /** (optional) The maxFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ + maxFeePerGas?: Quantity + /** (optional) The maxPriorityFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ + maxPriorityFeePerGas?: Quantity + }> + estimateGas( + transactionData: TransactionData, + chainId: SupportedChainId, + ownerIndex?: number, + ): Promise +} + +/** + * Options for getting all wallets + * @description Parameters for filtering and paginating wallet results + */ +export interface GetAllWalletsOptions { + /** Maximum number of wallets to return */ + limit?: number + /** Cursor for pagination */ + cursor?: string } diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index 368ceb3a..bb6e87f4 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -1,30 +1,32 @@ -import { createPublicClient, http, type PublicClient } from 'viem' +import { chainById } from '@eth-optimism/viem/chains' +import type { Address, Hash, PublicClient } from 'viem' +import { + createPublicClient, + createWalletClient, + encodeAbiParameters, + http, + parseEventLogs, +} from 'viem' +import { privateKeyToAccount } from 'viem/accounts' import { mainnet, unichain } from 'viem/chains' +import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' +import { smartWalletFactoryAddress } from '@/constants/addresses.js' import { LendProviderMorpho } from '@/lend/index.js' import { ChainManager } from '@/services/ChainManager.js' import type { LendProvider } from '@/types/lend.js' import type { VerbsConfig, VerbsInterface } from '@/types/verbs.js' -import type { - GetAllWalletsOptions, - Wallet, - WalletProvider, -} from '@/types/wallet.js' -import { WalletProviderPrivy } from '@/wallet/providers/privy.js' +import type { Wallet as WalletInterface } from '@/types/wallet.js' +import { Wallet } from '@/wallet/index.js' /** * Main Verbs SDK class * @description Core implementation of the Verbs SDK */ export class Verbs implements VerbsInterface { - // TODO Move to wallet provider - createWallet!: (userId: string) => Promise - getWallet!: (userId: string) => Promise - getAllWallets!: (options?: GetAllWalletsOptions) => Promise - - private walletProvider: WalletProvider private _chainManager: ChainManager private lendProvider?: LendProvider + private privateKey?: Hash constructor(config: VerbsConfig) { this._chainManager = new ChainManager( @@ -35,6 +37,7 @@ export class Verbs implements VerbsInterface { }, ], ) + this.privateKey = config.privateKey // Create lending provider if configured if (config.lend) { // TODO: delete this code and just have the lend use the ChainManager @@ -55,16 +58,75 @@ export class Verbs implements VerbsInterface { ) } } + } - this.walletProvider = this.createWalletProvider(config) + async createWallet( + ownerAddresses: Address[], + nonce?: bigint, + ): Promise> { + // deploy the wallet on each chain in the chain manager + const deployments = await Promise.all( + this._chainManager.getSupportedChains().map(async (chainId) => { + const walletClient = createWalletClient({ + chain: chainById[chainId], + transport: http(this._chainManager.getRpcUrl(chainId)), + account: privateKeyToAccount(this.privateKey!), + }) + const encodedOwners = ownerAddresses.map((ownerAddress) => + encodeAbiParameters([{ type: 'address' }], [ownerAddress]), + ) + const tx = await walletClient.writeContract({ + abi: smartWalletFactoryAbi, + address: smartWalletFactoryAddress, + functionName: 'createAccount', + args: [encodedOwners, nonce || 0n], + }) + const publicClient = this._chainManager.getPublicClient(chainId) + const receipt = await publicClient.waitForTransactionReceipt({ + hash: tx, + }) + if (!receipt.status) { + throw new Error('Wallet deployment failed') + } + // parse logs + const logs = parseEventLogs({ + abi: smartWalletFactoryAbi, + eventName: 'AccountCreated', + logs: receipt.logs, + }) + return { + chainId, + address: logs[0].args.account, + } + }), + ) + return deployments + } - // Delegate wallet methods to wallet provider - this.createWallet = this.walletProvider.createWallet.bind( - this.walletProvider, + async getWallet( + initialOwnerAddresses: Address[], + nonce?: bigint, + currentOwnerAddresses?: Address[], + ): Promise { + // Factory is the same accross all chains, so we can use the first chain to get the wallet address + const publicClient = this._chainManager.getPublicClient( + this._chainManager.getSupportedChains()[0], + ) + const encodedOwners = initialOwnerAddresses.map((ownerAddress) => + encodeAbiParameters([{ type: 'address' }], [ownerAddress]), ) - this.getWallet = this.walletProvider.getWallet.bind(this.walletProvider) - this.getAllWallets = this.walletProvider.getAllWallets.bind( - this.walletProvider, + const smartWalletAddress = await publicClient.readContract({ + abi: smartWalletFactoryAbi, + address: smartWalletFactoryAddress, + functionName: 'getAddress', + args: [encodedOwners, nonce || 0n], + }) + const owners = currentOwnerAddresses || initialOwnerAddresses + return new Wallet( + smartWalletAddress, + owners, + this._chainManager, + this.lendProvider!, ) } @@ -86,21 +148,6 @@ export class Verbs implements VerbsInterface { get chainManager(): ChainManager { return this._chainManager } - - private createWalletProvider(config: VerbsConfig): WalletProvider { - const { wallet } = config - - switch (wallet.type) { - case 'privy': - return new WalletProviderPrivy( - wallet.appId, - wallet.appSecret, - this, // Pass Verbs instance so wallets can access configured providers - ) - default: - throw new Error(`Unsupported wallet provider type: ${wallet.type}`) - } - } } /** diff --git a/packages/sdk/src/wallet/index.spec.ts b/packages/sdk/src/wallet/index.spec.ts index 92077052..e062ac99 100644 --- a/packages/sdk/src/wallet/index.spec.ts +++ b/packages/sdk/src/wallet/index.spec.ts @@ -1,163 +1,163 @@ -import type { Address } from 'viem' -import { unichain } from 'viem/chains' -import { describe, expect, it } from 'vitest' - -import type { ChainManager } from '@/services/ChainManager.js' -import { MockChainManager } from '@/test/MockChainManager.js' -import type { LendProvider } from '@/types/lend.js' -import type { VerbsInterface } from '@/types/verbs.js' -import { Wallet } from '@/wallet/index.js' - -describe('Wallet', () => { - const mockAddress: Address = '0x1234567890123456789012345678901234567890' - const mockId = 'test-wallet-id' - const chainManager: ChainManager = new MockChainManager({ - supportedChains: [unichain.id], - defaultBalance: 1000000n, - }) as any - - // Mock Verbs instance - const mockVerbs: VerbsInterface = { - get chainManager() { - return chainManager - }, - get lend(): LendProvider { - throw new Error('Lend provider not configured') - }, - createWallet: async () => { - throw new Error('Not implemented') - }, - getWallet: async () => { - throw new Error('Not implemented') - }, - getAllWallets: async () => { - throw new Error('Not implemented') - }, - } as VerbsInterface - - describe('constructor', () => { - it('should create a wallet instance with correct properties', () => { - const wallet = new Wallet(mockId, mockVerbs) - wallet.init(mockAddress) - - expect(wallet.id).toBe(mockId) - expect(wallet.address).toBe(mockAddress) - }) - - it('should handle different wallet IDs', () => { - const ids = ['wallet-1', 'wallet-2', 'test-id-123'] - - ids.forEach((id) => { - const wallet = new Wallet(id, mockVerbs) - expect(wallet.id).toBe(id) - }) - }) - }) - - describe('address assignment', () => { - it('should allow setting address after creation', () => { - const addresses: Address[] = [ - '0x0000000000000000000000000000000000000000', - '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF', - '0x742d35Cc6634C0532925a3b8C17Eb02c7b2BD8eB', - ] - - addresses.forEach((address) => { - const wallet = new Wallet(mockId, mockVerbs) - wallet.init(address) - expect(wallet.address).toBe(address) - expect(wallet.id).toBe(mockId) - }) - }) - }) - - describe('getBalance', () => { - it('should return token balances', async () => { - const wallet = new Wallet(mockId, mockVerbs) - wallet.init(mockAddress) - - const balance = await wallet.getBalance() - - expect(balance).toEqual([ - { - totalBalance: 1000000n, - symbol: 'ETH', - totalFormattedBalance: '0.000000000001', - chainBalances: [ - { - balance: 1000000n, - chainId: 130, - formattedBalance: '0.000000000001', - }, - ], - }, - { - totalBalance: 1000000n, - symbol: 'ETH', - totalFormattedBalance: '0.000000000001', - chainBalances: [ - { - balance: 1000000n, - chainId: 130, - formattedBalance: '0.000000000001', - }, - ], - }, - { - totalBalance: 1000000n, - symbol: 'USDC', - totalFormattedBalance: '1', - chainBalances: [ - { - balance: 1000000n, - chainId: 130, - formattedBalance: '1', - }, - ], - }, - { - totalBalance: 0n, - symbol: 'MORPHO', - totalFormattedBalance: '0', - chainBalances: [], - }, - ]) - }) - - it('should throw an error if the wallet is not initialized', async () => { - const wallet = new Wallet(mockId, mockVerbs) - await expect(wallet.getBalance()).rejects.toThrow( - 'Wallet not initialized', - ) - }) - }) - - describe('edge cases', () => { - it('should handle empty string id', () => { - const wallet = new Wallet('', mockVerbs) - expect(wallet.id).toBe('') - expect(wallet.address).toBeUndefined() - }) - - it('should handle very long wallet id', () => { - const longId = 'a'.repeat(1000) - const wallet = new Wallet(longId, mockVerbs) - expect(wallet.id).toBe(longId) - expect(wallet.id.length).toBe(1000) - }) - }) - - describe('immutability', () => { - it('should maintain property values after creation', () => { - const wallet = new Wallet(mockId, mockVerbs) - wallet.address = mockAddress - - const originalId = wallet.id - const originalAddress = wallet.address - - // Properties should remain unchanged - expect(wallet.id).toBe(originalId) - expect(wallet.address).toBe(originalAddress) - }) - }) -}) +// import type { Address } from 'viem' +// import { unichain } from 'viem/chains' +// import { describe, expect, it } from 'vitest' + +// import type { ChainManager } from '@/services/ChainManager.js' +// import { MockChainManager } from '@/test/MockChainManager.js' +// import type { LendProvider } from '@/types/lend.js' +// import type { VerbsInterface } from '@/types/verbs.js' +// import { Wallet } from '@/wallet/index.js' + +// describe('Wallet', () => { +// const mockAddress: Address = '0x1234567890123456789012345678901234567890' +// const mockId = 'test-wallet-id' +// const chainManager: ChainManager = new MockChainManager({ +// supportedChains: [unichain.id], +// defaultBalance: 1000000n, +// }) as any + +// // Mock Verbs instance +// const mockVerbs: VerbsInterface = { +// get chainManager() { +// return chainManager +// }, +// get lend(): LendProvider { +// throw new Error('Lend provider not configured') +// }, +// createWallet: async () => { +// throw new Error('Not implemented') +// }, +// getWallet: async () => { +// throw new Error('Not implemented') +// }, +// getAllWallets: async () => { +// throw new Error('Not implemented') +// }, +// } as VerbsInterface + +// describe('constructor', () => { +// it('should create a wallet instance with correct properties', () => { +// const wallet = new Wallet(mockId, mockVerbs) +// wallet.init(mockAddress) + +// expect(wallet.id).toBe(mockId) +// expect(wallet.address).toBe(mockAddress) +// }) + +// it('should handle different wallet IDs', () => { +// const ids = ['wallet-1', 'wallet-2', 'test-id-123'] + +// ids.forEach((id) => { +// const wallet = new Wallet(id, mockVerbs) +// expect(wallet.id).toBe(id) +// }) +// }) +// }) + +// describe('address assignment', () => { +// it('should allow setting address after creation', () => { +// const addresses: Address[] = [ +// '0x0000000000000000000000000000000000000000', +// '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF', +// '0x742d35Cc6634C0532925a3b8C17Eb02c7b2BD8eB', +// ] + +// addresses.forEach((address) => { +// const wallet = new Wallet(mockId, mockVerbs) +// wallet.init(address) +// expect(wallet.address).toBe(address) +// expect(wallet.id).toBe(mockId) +// }) +// }) +// }) + +// describe('getBalance', () => { +// it('should return token balances', async () => { +// const wallet = new Wallet(mockId, mockVerbs) +// wallet.init(mockAddress) + +// const balance = await wallet.getBalance() + +// expect(balance).toEqual([ +// { +// totalBalance: 1000000n, +// symbol: 'ETH', +// totalFormattedBalance: '0.000000000001', +// chainBalances: [ +// { +// balance: 1000000n, +// chainId: 130, +// formattedBalance: '0.000000000001', +// }, +// ], +// }, +// { +// totalBalance: 1000000n, +// symbol: 'ETH', +// totalFormattedBalance: '0.000000000001', +// chainBalances: [ +// { +// balance: 1000000n, +// chainId: 130, +// formattedBalance: '0.000000000001', +// }, +// ], +// }, +// { +// totalBalance: 1000000n, +// symbol: 'USDC', +// totalFormattedBalance: '1', +// chainBalances: [ +// { +// balance: 1000000n, +// chainId: 130, +// formattedBalance: '1', +// }, +// ], +// }, +// { +// totalBalance: 0n, +// symbol: 'MORPHO', +// totalFormattedBalance: '0', +// chainBalances: [], +// }, +// ]) +// }) + +// it('should throw an error if the wallet is not initialized', async () => { +// const wallet = new Wallet(mockId, mockVerbs) +// await expect(wallet.getBalance()).rejects.toThrow( +// 'Wallet not initialized', +// ) +// }) +// }) + +// describe('edge cases', () => { +// it('should handle empty string id', () => { +// const wallet = new Wallet('', mockVerbs) +// expect(wallet.id).toBe('') +// expect(wallet.address).toBeUndefined() +// }) + +// it('should handle very long wallet id', () => { +// const longId = 'a'.repeat(1000) +// const wallet = new Wallet(longId, mockVerbs) +// expect(wallet.id).toBe(longId) +// expect(wallet.id.length).toBe(1000) +// }) +// }) + +// describe('immutability', () => { +// it('should maintain property values after creation', () => { +// const wallet = new Wallet(mockId, mockVerbs) +// wallet.address = mockAddress + +// const originalId = wallet.id +// const originalAddress = wallet.address + +// // Properties should remain unchanged +// expect(wallet.id).toBe(originalId) +// expect(wallet.address).toBe(originalAddress) +// }) +// }) +// }) diff --git a/packages/sdk/src/wallet/index.ts b/packages/sdk/src/wallet/index.ts index 96e0a715..b09e5da5 100644 --- a/packages/sdk/src/wallet/index.ts +++ b/packages/sdk/src/wallet/index.ts @@ -1,6 +1,10 @@ -import { type Address, encodeFunctionData, erc20Abi, type Hash } from 'viem' +import type { Address, Hash, Hex, PublicClient, Quantity } from 'viem' +import { encodeFunctionData, erc20Abi } from 'viem' import { unichain } from 'viem/chains' +import { smartWalletAbi } from '@/abis/smartWallet.js' +import type { SupportedChainId } from '@/constants/supportedChains.js' +import type { ChainManager } from '@/services/ChainManager.js' import { fetchERC20Balance, fetchETHBalance } from '@/services/tokenBalance.js' import { SUPPORTED_TOKENS } from '@/supported/tokens.js' import type { @@ -10,7 +14,6 @@ import type { TransactionData, } from '@/types/lend.js' import type { TokenBalance } from '@/types/token.js' -import type { VerbsInterface } from '@/types/verbs.js' import type { Wallet as WalletInterface } from '@/types/wallet.js' import { type AssetIdentifier, @@ -24,27 +27,26 @@ import { * @description Concrete implementation of the Wallet interface */ export class Wallet implements WalletInterface { - id: string - address!: Address - private lendProvider?: LendProvider - private initialized: boolean = false - private verbs: VerbsInterface - private walletProvider: any // Store reference to wallet provider for signing + public ownerAddresses: Address[] + public address: Address + private lendProvider: LendProvider + private chainManager: ChainManager /** * Create a new wallet instance * @param id - Unique wallet identifier * @param verbs - Verbs instance to access configured providers and chain manager */ - constructor(id: string, verbs: VerbsInterface, walletProvider?: any) { - this.id = id - this.verbs = verbs - this.walletProvider = walletProvider - } - - init(address: Address) { + constructor( + address: Address, + ownerAddresses: Address[], + chainManager: ChainManager, + lendProvider: LendProvider, + ) { this.address = address - this.initialized = true + this.ownerAddresses = ownerAddresses + this.chainManager = chainManager + this.lendProvider = lendProvider } /** @@ -52,19 +54,12 @@ export class Wallet implements WalletInterface { * @returns Promise resolving to array of asset balances */ async getBalance(): Promise { - if (!this.initialized) { - throw new Error('Wallet not initialized') - } - const tokenBalancePromises = Object.values(SUPPORTED_TOKENS).map( async (token) => { - return fetchERC20Balance(this.verbs.chainManager, this.address, token) + return fetchERC20Balance(this.chainManager, this.address, token) }, ) - const ethBalancePromise = fetchETHBalance( - this.verbs.chainManager, - this.address, - ) + const ethBalancePromise = fetchETHBalance(this.chainManager, this.address) return Promise.all([ethBalancePromise, ...tokenBalancePromises]) } @@ -85,10 +80,6 @@ export class Wallet implements WalletInterface { marketId?: string, options?: LendOptions, ): Promise { - if (!this.initialized) { - throw new Error('Wallet not initialized') - } - // Parse human-readable inputs // TODO: Get actual chain ID from wallet context, for now using Unichain const { amount: parsedAmount, asset: resolvedAsset } = parseLendParams( @@ -103,7 +94,7 @@ export class Wallet implements WalletInterface { receiver: options?.receiver || this.address, } - const result = await this.verbs.lend.deposit( + const result = await this.lendProvider.deposit( resolvedAsset.address, parsedAmount, marketId, @@ -113,47 +104,89 @@ export class Wallet implements WalletInterface { return result } - /** - * Sign and send a transaction - * @description Signs and sends a transaction using the configured wallet provider - * @param transactionData - Transaction data to sign and send - * @returns Promise resolving to transaction hash - * @throws Error if wallet is not initialized or no wallet provider is configured - */ - async signAndSend(transactionData: TransactionData): Promise { - if (!this.initialized) { - throw new Error('Wallet not initialized') - } + execute(transactionData: TransactionData): Hash { + return encodeFunctionData({ + abi: smartWalletAbi, + functionName: 'execute', + args: [transactionData.to, transactionData.value, transactionData.data], + }) + } - if (!this.walletProvider || !this.walletProvider.sign) { - throw new Error('Wallet provider does not support transaction signing') - } + async getTxParams( + transactionData: TransactionData, + chainId: SupportedChainId, + ownerIndex: number = 0, + ): Promise<{ + /** The address the transaction is sent from. Must be hexadecimal formatted. */ + from?: Hex + /** Destination address of the transaction. */ + to?: Hex + /** The nonce to be used for the transaction (hexadecimal or number). */ + nonce?: Quantity + /** (optional) The chain ID of network your transaction will be sent on. */ + chainId?: Quantity + /** (optional) Data to send to the receiving address, especially when calling smart contracts. Must be hexadecimal formatted. */ + data?: Hex + /** (optional) The value (in wei) be sent with the transaction (hexadecimal or number). */ + value?: Quantity + /** (optional) The EIP-2718 transction type (e.g. `2` for EIP-1559 transactions). */ + type?: 0 | 1 | 2 + /** (optional) The max units of gas that can be used by this transaction (hexadecimal or number). */ + gasLimit?: Quantity + /** (optional) The price (in wei) per unit of gas for this transaction (hexadecimal or number), for use in non EIP-1559 transactions (type 0 or 1). */ + gasPrice?: Quantity + /** (optional) The maxFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ + maxFeePerGas?: Quantity + /** (optional) The maxPriorityFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ + maxPriorityFeePerGas?: Quantity + }> { + const executeCallData = this.execute(transactionData) - return this.walletProvider.sign(this.id, transactionData) - } + const publicClient = this.chainManager.getPublicClient(chainId) - /** - * Sign a transaction without sending it - * @description Signs a transaction using the configured wallet provider but doesn't send it - * @param transactionData - Transaction data to sign - * @returns Promise resolving to signed transaction - * @throws Error if wallet is not initialized or no wallet provider is configured - */ - async sign(transactionData: TransactionData): Promise<`0x${string}`> { - if (!this.initialized) { - throw new Error('Wallet not initialized') - } + // Estimate gas limit + const gasLimit = await publicClient.estimateGas({ + account: this.ownerAddresses[ownerIndex], + to: this.address, + data: executeCallData, + value: BigInt(transactionData.value), + }) - if (!this.walletProvider || !(this.walletProvider as any).signOnly) { - throw new Error( - 'Wallet provider does not support transaction signing only', - ) + // Get current gas price and fee data + const feeData = await publicClient.estimateFeesPerGas() + + // Get current nonce for the wallet - manual management since Privy isn't handling it properly + const nonce = await publicClient.getTransactionCount({ + address: this.ownerAddresses[ownerIndex], + blockTag: 'pending', // Use pending to get the next nonce including any pending txs + }) + + return { + to: this.address, + data: executeCallData, + value: transactionData.value as `0x${string}`, + chainId: `0x${chainId.toString(16)}`, + type: 2, // EIP-1559 + gasLimit: `0x${gasLimit.toString(16)}`, + maxFeePerGas: `0x${(feeData.maxFeePerGas || BigInt(1000000000)).toString(16)}`, // fallback to 1 gwei + maxPriorityFeePerGas: `0x${(feeData.maxPriorityFeePerGas || BigInt(100000000)).toString(16)}`, // fallback to 0.1 gwei + nonce: `0x${nonce.toString(16)}`, // Explicitly provide the correct nonce } + } - return (this.walletProvider as any).signOnly( - this.id, - transactionData, - ) as `0x${string}` + async estimateGas( + transactionData: TransactionData, + chainId: SupportedChainId, + ownerIndex: number = 0, + ): Promise { + const executeCallData = this.execute(transactionData) + const publicClient = this.chainManager.getPublicClient(chainId) + return publicClient.estimateGas({ + account: this.ownerAddresses[ownerIndex], + to: this.address, + data: executeCallData, + value: BigInt(transactionData.value), + }) } /** @@ -163,7 +196,10 @@ export class Wallet implements WalletInterface { * @param publicClient - Viem public client to send the transaction * @returns Promise resolving to transaction hash */ - async send(signedTransaction: string, publicClient: any): Promise { + async send( + signedTransaction: string, + publicClient: PublicClient, + ): Promise { try { const hash = await publicClient.sendRawTransaction({ serializedTransaction: signedTransaction as `0x${string}`, @@ -192,10 +228,6 @@ export class Wallet implements WalletInterface { asset: AssetIdentifier, recipientAddress: Address, ): Promise { - if (!this.initialized) { - throw new Error('Wallet not initialized') - } - if (!recipientAddress) { throw new Error('Recipient address is required') } diff --git a/packages/sdk/src/wallet/providers/privy.ts b/packages/sdk/src/wallet/providers/privy.ts index 124699f3..ce6a5b32 100644 --- a/packages/sdk/src/wallet/providers/privy.ts +++ b/packages/sdk/src/wallet/providers/privy.ts @@ -1,195 +1,195 @@ -import { PrivyClient } from '@privy-io/server-auth' -import { getAddress, type Hash } from 'viem' - -import { Wallet } from '@/index.js' -import type { TransactionData } from '@/types/lend.js' -import type { VerbsInterface } from '@/types/verbs.js' -import type { GetAllWalletsOptions, WalletProvider } from '@/types/wallet.js' - -/** - * Privy wallet provider implementation - * @description Wallet provider implementation using Privy service - */ -export class WalletProviderPrivy implements WalletProvider { - private privy: PrivyClient - private verbs: VerbsInterface - - /** - * Create a new Privy wallet provider - * @param appId - Privy application ID - * @param appSecret - Privy application secret - * @param verbs - Verbs instance for accessing configured providers - */ - constructor(appId: string, appSecret: string, verbs: VerbsInterface) { - this.privy = new PrivyClient(appId, appSecret) - this.verbs = verbs - } - - /** - * Create new wallet via Privy - * @description Creates a new wallet using Privy's wallet API - * @param userId - User identifier for the wallet - * @returns Promise resolving to new wallet instance - * @throws Error if wallet creation fails - */ - async createWallet(userId: string): Promise { - try { - const wallet = await this.privy.walletApi.createWallet({ - chainType: 'ethereum', - }) - - const walletInstance = new Wallet(wallet.id, this.verbs, this) - walletInstance.init(getAddress(wallet.address)) - return walletInstance - } catch { - throw new Error(`Failed to create wallet for user ${userId}`) - } - } - - /** - * Get wallet by user ID via Privy - * @description Retrieves wallet information from Privy service - * @param userId - User identifier - * @returns Promise resolving to wallet or null if not found - */ - async getWallet(userId: string): Promise { - try { - // TODO: Implement proper user-to-wallet lookup - const wallet = await this.privy.walletApi.getWallet({ id: userId }) - - const walletInstance = new Wallet(wallet.id, this.verbs, this) - walletInstance.init(getAddress(wallet.address)) - return walletInstance - } catch { - return null - } - } - - /** - * Get all wallets via Privy - * @description Retrieves all wallets from Privy service with optional filtering - * @param options - Optional parameters for filtering and pagination - * @returns Promise resolving to array of wallets - */ - async getAllWallets(options?: GetAllWalletsOptions): Promise { - try { - const response = await this.privy.walletApi.getWallets({ - limit: options?.limit, - cursor: options?.cursor, - }) - - return response.data.map((wallet) => { - const walletInstance = new Wallet(wallet.id, this.verbs, this) - walletInstance.init(getAddress(wallet.address)) - return walletInstance - }) - } catch { - throw new Error('Failed to retrieve wallets') - } - } - - /** - * Sign and send a transaction using Privy - * @description Signs and sends a transaction using Privy's wallet API - * @param walletId - Wallet ID to use for signing - * @param transactionData - Transaction data to sign and send - * @returns Promise resolving to transaction hash - * @throws Error if transaction signing fails - */ - async sign( - walletId: string, - transactionData: TransactionData, - ): Promise { - try { - const response = await this.privy.walletApi.ethereum.sendTransaction({ - walletId, - caip2: 'eip155:130', // Unichain - transaction: { - to: transactionData.to, - data: transactionData.data as `0x${string}`, - value: Number(transactionData.value), - chainId: 130, // Unichain - }, - }) - - return response.hash as Hash - } catch (error) { - throw new Error( - `Failed to sign 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 - * @param walletId - Wallet ID to use for signing - * @param transactionData - Transaction data to sign - * @returns Promise resolving to signed transaction - * @throws Error if transaction signing fails - */ - async signOnly( - walletId: string, - transactionData: TransactionData, - ): Promise { - try { - // Get wallet to determine the from address for gas estimation - const wallet = await this.getWallet(walletId) - if (!wallet) { - throw new Error(`Wallet not found: ${walletId}`) - } - - // Get public client for gas estimation - const publicClient = this.verbs.chainManager.getPublicClient(130) // Unichain - - // Estimate gas limit - const gasLimit = await publicClient.estimateGas({ - account: wallet.address, - to: transactionData.to, - data: transactionData.data as `0x${string}`, - value: BigInt(transactionData.value), - }) - - // Get current gas price and fee data - const feeData = await publicClient.estimateFeesPerGas() - - // Get current nonce for the wallet - manual management since Privy isn't handling it properly - const nonce = await publicClient.getTransactionCount({ - address: wallet.address, - blockTag: 'pending', // Use pending to get the next nonce including any pending txs - }) - - // According to Privy docs: if you provide ANY gas parameters, you must provide ALL of them - const txParams: any = { - to: transactionData.to, - data: transactionData.data as `0x${string}`, - value: transactionData.value as `0x${string}`, - chainId: 130, // Unichain - type: 2, // EIP-1559 - gasLimit: `0x${gasLimit.toString(16)}`, - maxFeePerGas: `0x${(feeData.maxFeePerGas || BigInt(1000000000)).toString(16)}`, // fallback to 1 gwei - maxPriorityFeePerGas: `0x${(feeData.maxPriorityFeePerGas || BigInt(100000000)).toString(16)}`, // fallback to 0.1 gwei - 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, - transaction: txParams, - }) - - return response.signedTransaction - } catch (error) { - throw new Error( - `Failed to sign transaction for wallet ${walletId}: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - ) - } - } -} +// import { PrivyClient } from '@privy-io/server-auth' +// import { getAddress, type Hash } from 'viem' + +// import { Wallet } from '@/index.js' +// import type { TransactionData } from '@/types/lend.js' +// import type { VerbsInterface } from '@/types/verbs.js' +// import type { GetAllWalletsOptions, WalletProvider } from '@/types/wallet.js' + +// /** +// * Privy wallet provider implementation +// * @description Wallet provider implementation using Privy service +// */ +// export class WalletProviderPrivy implements WalletProvider { +// private privy: PrivyClient +// private verbs: VerbsInterface + +// /** +// * Create a new Privy wallet provider +// * @param appId - Privy application ID +// * @param appSecret - Privy application secret +// * @param verbs - Verbs instance for accessing configured providers +// */ +// constructor(appId: string, appSecret: string, verbs: VerbsInterface) { +// this.privy = new PrivyClient(appId, appSecret) +// this.verbs = verbs +// } + +// /** +// * Create new wallet via Privy +// * @description Creates a new wallet using Privy's wallet API +// * @param userId - User identifier for the wallet +// * @returns Promise resolving to new wallet instance +// * @throws Error if wallet creation fails +// */ +// async createWallet(userId: string): Promise { +// try { +// const wallet = await this.privy.walletApi.createWallet({ +// chainType: 'ethereum', +// }) + +// const walletInstance = new Wallet(wallet.id, this.verbs, this) +// walletInstance.init(getAddress(wallet.address)) +// return walletInstance +// } catch { +// throw new Error(`Failed to create wallet for user ${userId}`) +// } +// } + +// /** +// * Get wallet by user ID via Privy +// * @description Retrieves wallet information from Privy service +// * @param userId - User identifier +// * @returns Promise resolving to wallet or null if not found +// */ +// async getWallet(userId: string): Promise { +// try { +// // TODO: Implement proper user-to-wallet lookup +// const wallet = await this.privy.walletApi.getWallet({ id: userId }) + +// const walletInstance = new Wallet(wallet.id, this.verbs.chainManager, this.verbs.lendProvider!) +// walletInstance.init(getAddress(wallet.address)) +// return walletInstance +// } catch { +// return null +// } +// } + +// /** +// * Get all wallets via Privy +// * @description Retrieves all wallets from Privy service with optional filtering +// * @param options - Optional parameters for filtering and pagination +// * @returns Promise resolving to array of wallets +// */ +// async getAllWallets(options?: GetAllWalletsOptions): Promise { +// try { +// const response = await this.privy.walletApi.getWallets({ +// limit: options?.limit, +// cursor: options?.cursor, +// }) + +// return response.data.map((wallet) => { +// const walletInstance = new Wallet(wallet.id, this.verbs, this) +// walletInstance.init(getAddress(wallet.address)) +// return walletInstance +// }) +// } catch { +// throw new Error('Failed to retrieve wallets') +// } +// } + +// /** +// * Sign and send a transaction using Privy +// * @description Signs and sends a transaction using Privy's wallet API +// * @param walletId - Wallet ID to use for signing +// * @param transactionData - Transaction data to sign and send +// * @returns Promise resolving to transaction hash +// * @throws Error if transaction signing fails +// */ +// async sign( +// walletId: string, +// transactionData: TransactionData, +// ): Promise { +// try { +// const response = await this.privy.walletApi.ethereum.sendTransaction({ +// walletId, +// caip2: 'eip155:130', // Unichain +// transaction: { +// to: transactionData.to, +// data: transactionData.data as `0x${string}`, +// value: Number(transactionData.value), +// chainId: 130, // Unichain +// }, +// }) + +// return response.hash as Hash +// } catch (error) { +// throw new Error( +// `Failed to sign 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 +// * @param walletId - Wallet ID to use for signing +// * @param transactionData - Transaction data to sign +// * @returns Promise resolving to signed transaction +// * @throws Error if transaction signing fails +// */ +// async signOnly( +// walletId: string, +// transactionData: TransactionData, +// ): Promise { +// try { +// // Get wallet to determine the from address for gas estimation +// const wallet = await this.getWallet(walletId) +// if (!wallet) { +// throw new Error(`Wallet not found: ${walletId}`) +// } + +// // Get public client for gas estimation +// const publicClient = this.verbs.chainManager.getPublicClient(130) // Unichain + +// // Estimate gas limit +// const gasLimit = await publicClient.estimateGas({ +// account: wallet.address, +// to: transactionData.to, +// data: transactionData.data as `0x${string}`, +// value: BigInt(transactionData.value), +// }) + +// // Get current gas price and fee data +// const feeData = await publicClient.estimateFeesPerGas() + +// // Get current nonce for the wallet - manual management since Privy isn't handling it properly +// const nonce = await publicClient.getTransactionCount({ +// address: wallet.address, +// blockTag: 'pending', // Use pending to get the next nonce including any pending txs +// }) + +// // According to Privy docs: if you provide ANY gas parameters, you must provide ALL of them +// const txParams: any = { +// to: transactionData.to, +// data: transactionData.data as `0x${string}`, +// value: transactionData.value as `0x${string}`, +// chainId: 130, // Unichain +// type: 2, // EIP-1559 +// gasLimit: `0x${gasLimit.toString(16)}`, +// maxFeePerGas: `0x${(feeData.maxFeePerGas || BigInt(1000000000)).toString(16)}`, // fallback to 1 gwei +// maxPriorityFeePerGas: `0x${(feeData.maxPriorityFeePerGas || BigInt(100000000)).toString(16)}`, // fallback to 0.1 gwei +// 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, +// transaction: txParams, +// }) + +// return response.signedTransaction +// } catch (error) { +// throw new Error( +// `Failed to sign transaction for wallet ${walletId}: ${ +// error instanceof Error ? error.message : 'Unknown error' +// }`, +// ) +// } +// } +// } diff --git a/packages/sdk/src/wallet/providers/test.ts b/packages/sdk/src/wallet/providers/test.ts index 2a74d256..690647ee 100644 --- a/packages/sdk/src/wallet/providers/test.ts +++ b/packages/sdk/src/wallet/providers/test.ts @@ -1,125 +1,125 @@ -import { type Address, type Hash, type WalletClient } from 'viem' - -import type { TransactionData } from '@/types/lend.js' -import type { VerbsInterface } from '@/types/verbs.js' -import type { - GetAllWalletsOptions, - Wallet, - WalletProvider, -} from '@/types/wallet.js' -import { Wallet as WalletImpl } from '@/wallet/index.js' - -/** - * Test wallet provider for local testing with viem - * @description Test implementation of WalletProvider that uses viem directly - */ -export class WalletProviderTest implements WalletProvider { - private walletClient: WalletClient - private verbs: VerbsInterface - private wallets: Map = new Map() - - constructor(walletClient: WalletClient, verbs: VerbsInterface) { - this.walletClient = walletClient - this.verbs = verbs - } - - /** - * Create a new wallet (or register existing) - */ - async createWallet(userId: string): Promise { - // For testing, we'll use the wallet client's account - const [address] = await this.walletClient.getAddresses() - - this.wallets.set(userId, { id: userId, address }) - - const wallet = new WalletImpl(userId, this.verbs, this) - wallet.init(address) - return wallet - } - - /** - * Get wallet by user ID - */ - async getWallet(userId: string): Promise { - const walletData = this.wallets.get(userId) - if (!walletData) return null - - const wallet = new WalletImpl(userId, this.verbs, this) - wallet.init(walletData.address) - return wallet - } - - /** - * Get all wallets - */ - async getAllWallets(_options?: GetAllWalletsOptions): Promise { - const wallets: Wallet[] = [] - - for (const [userId, walletData] of this.wallets.entries()) { - const wallet = new WalletImpl(userId, this.verbs, this) - wallet.init(walletData.address) - wallets.push(wallet) - } - - return wallets - } - - /** - * Sign and send a transaction using viem wallet client - */ - async sign( - _walletId: string, - transactionData: TransactionData, - ): Promise { - try { - // Send transaction using viem wallet client - const txParams: any = { - to: transactionData.to as Address, - data: transactionData.data as `0x${string}`, - value: BigInt(transactionData.value), - } - - // Add account if available - if (this.walletClient.account) { - txParams.account = this.walletClient.account - } - - const hash = await this.walletClient.sendTransaction(txParams) - - return hash - } catch (error) { - throw new Error( - `Failed to sign transaction: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - ) - } - } - - /** - * Sign a transaction without sending it - */ - async signOnly( - _walletId: string, - transactionData: TransactionData, - ): Promise { - try { - // Sign transaction using viem wallet client - const txParams: any = { - to: transactionData.to as Address, - data: transactionData.data as `0x${string}`, - value: BigInt(transactionData.value), - } - - const signedTx = await this.walletClient.signTransaction(txParams) - - return signedTx - } catch (error) { - throw new Error( - `Failed to sign transaction: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - ) - } - } -} +// import { type Address, type Hash, type WalletClient } from 'viem' + +// import type { TransactionData } from '@/types/lend.js' +// import type { VerbsInterface } from '@/types/verbs.js' +// import type { +// GetAllWalletsOptions, +// Wallet, +// WalletProvider, +// } from '@/types/wallet.js' +// import { Wallet as WalletImpl } from '@/wallet/index.js' + +// /** +// * Test wallet provider for local testing with viem +// * @description Test implementation of WalletProvider that uses viem directly +// */ +// export class WalletProviderTest implements WalletProvider { +// private walletClient: WalletClient +// private verbs: VerbsInterface +// private wallets: Map = new Map() + +// constructor(walletClient: WalletClient, verbs: VerbsInterface) { +// this.walletClient = walletClient +// this.verbs = verbs +// } + +// /** +// * Create a new wallet (or register existing) +// */ +// async createWallet(userId: string): Promise { +// // For testing, we'll use the wallet client's account +// const [address] = await this.walletClient.getAddresses() + +// this.wallets.set(userId, { id: userId, address }) + +// const wallet = new WalletImpl(userId, this.verbs, this) +// wallet.init(address) +// return wallet +// } + +// /** +// * Get wallet by user ID +// */ +// async getWallet(userId: string): Promise { +// const walletData = this.wallets.get(userId) +// if (!walletData) return null + +// const wallet = new WalletImpl(userId, this.verbs, this) +// wallet.init(walletData.address) +// return wallet +// } + +// /** +// * Get all wallets +// */ +// async getAllWallets(_options?: GetAllWalletsOptions): Promise { +// const wallets: Wallet[] = [] + +// for (const [userId, walletData] of this.wallets.entries()) { +// const wallet = new WalletImpl(userId, this.verbs, this) +// wallet.init(walletData.address) +// wallets.push(wallet) +// } + +// return wallets +// } + +// /** +// * Sign and send a transaction using viem wallet client +// */ +// async sign( +// _walletId: string, +// transactionData: TransactionData, +// ): Promise { +// try { +// // Send transaction using viem wallet client +// const txParams: any = { +// to: transactionData.to as Address, +// data: transactionData.data as `0x${string}`, +// value: BigInt(transactionData.value), +// } + +// // Add account if available +// if (this.walletClient.account) { +// txParams.account = this.walletClient.account +// } + +// const hash = await this.walletClient.sendTransaction(txParams) + +// return hash +// } catch (error) { +// throw new Error( +// `Failed to sign transaction: ${ +// error instanceof Error ? error.message : 'Unknown error' +// }`, +// ) +// } +// } + +// /** +// * Sign a transaction without sending it +// */ +// async signOnly( +// _walletId: string, +// transactionData: TransactionData, +// ): Promise { +// try { +// // Sign transaction using viem wallet client +// const txParams: any = { +// to: transactionData.to as Address, +// data: transactionData.data as `0x${string}`, +// value: BigInt(transactionData.value), +// } + +// const signedTx = await this.walletClient.signTransaction(txParams) + +// return signedTx +// } catch (error) { +// throw new Error( +// `Failed to sign transaction: ${ +// error instanceof Error ? error.message : 'Unknown error' +// }`, +// ) +// } +// } +// } diff --git a/packages/sdk/src/wallet/wallet.test.ts b/packages/sdk/src/wallet/wallet.test.ts index 3e79efc8..32956b33 100644 --- a/packages/sdk/src/wallet/wallet.test.ts +++ b/packages/sdk/src/wallet/wallet.test.ts @@ -1,187 +1,187 @@ -import { type Address, encodeFunctionData, erc20Abi, parseUnits } from 'viem' -import { unichain } from 'viem/chains' -import { beforeEach, describe, expect, it } from 'vitest' - -import type { VerbsInterface } from '../types/verbs.js' -import { initVerbs } from '../verbs.js' -import { Wallet } from './index.js' - -describe('Wallet SendTokens', () => { - let verbs: VerbsInterface - let wallet: Wallet - const testAddress: Address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' - const recipientAddress: Address = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' - - beforeEach(() => { - // Initialize Verbs SDK - verbs = initVerbs({ - wallet: { - type: 'privy', - appId: 'test-app-id', - appSecret: 'test-app-secret', - }, - chains: [ - { - chainId: unichain.id, - rpcUrl: 'http://localhost:9546', - }, - ], - }) - - // Create wallet instance - wallet = new Wallet('test-wallet', verbs) - wallet.init(testAddress) - }) - - describe('ETH transfers', () => { - it('should create valid ETH transfer transaction data', async () => { - const transferData = await wallet.sendTokens(1.5, 'eth', recipientAddress) - - expect(transferData).toEqual({ - to: recipientAddress, - value: `0x${parseUnits('1.5', 18).toString(16)}`, - data: '0x', - }) - }) - - it('should handle fractional ETH amounts correctly', async () => { - const transferData = await wallet.sendTokens( - 0.001, - 'eth', - recipientAddress, - ) - const expectedValue = parseUnits('0.001', 18) - - expect(transferData.to).toBe(recipientAddress) - expect(transferData.value).toBe(`0x${expectedValue.toString(16)}`) - expect(transferData.data).toBe('0x') - }) - - it('should handle large ETH amounts correctly', async () => { - const transferData = await wallet.sendTokens(100, 'eth', recipientAddress) - const expectedValue = parseUnits('100', 18) - - expect(transferData.to).toBe(recipientAddress) - expect(transferData.value).toBe(`0x${expectedValue.toString(16)}`) - expect(transferData.data).toBe('0x') - }) - }) - - describe('USDC transfers', () => { - const usdcAddress = '0x078d782b760474a361dda0af3839290b0ef57ad6' // USDC on Unichain - - it('should create valid USDC transfer transaction data', async () => { - const transferData = await wallet.sendTokens( - 100, - 'usdc', - recipientAddress, - ) - const expectedAmount = parseUnits('100', 6) // USDC has 6 decimals - - const expectedData = encodeFunctionData({ - abi: erc20Abi, - functionName: 'transfer', - args: [recipientAddress, expectedAmount], - }) - - expect(transferData).toEqual({ - to: usdcAddress, - value: '0x0', - data: expectedData, - }) - }) - - it('should handle fractional USDC amounts correctly', async () => { - const transferData = await wallet.sendTokens( - 0.5, - 'usdc', - recipientAddress, - ) - const expectedAmount = parseUnits('0.5', 6) // USDC has 6 decimals - - const expectedData = encodeFunctionData({ - abi: erc20Abi, - functionName: 'transfer', - args: [recipientAddress, expectedAmount], - }) - - expect(transferData.to).toBe(usdcAddress) - expect(transferData.value).toBe('0x0') - expect(transferData.data).toBe(expectedData) - }) - - it('should handle USDC by token address', async () => { - const transferData = await wallet.sendTokens( - 50, - usdcAddress, - recipientAddress, - ) - const expectedAmount = parseUnits('50', 6) // USDC has 6 decimals - - const expectedData = encodeFunctionData({ - abi: erc20Abi, - functionName: 'transfer', - args: [recipientAddress, expectedAmount], - }) - - expect(transferData.to).toBe(usdcAddress) - expect(transferData.value).toBe('0x0') - expect(transferData.data).toBe(expectedData) - }) - }) - - describe('validation', () => { - it('should throw error if wallet is not initialized', async () => { - const uninitializedWallet = new Wallet('test-wallet', verbs) - - await expect( - uninitializedWallet.sendTokens(1, 'eth', recipientAddress), - ).rejects.toThrow('Wallet not initialized') - }) - - it('should throw error for zero amount', async () => { - await expect( - wallet.sendTokens(0, 'eth', recipientAddress), - ).rejects.toThrow('Amount must be greater than 0') - }) - - it('should throw error for negative amount', async () => { - await expect( - wallet.sendTokens(-1, 'eth', recipientAddress), - ).rejects.toThrow('Amount must be greater than 0') - }) - - it('should throw error for empty recipient address', async () => { - await expect(wallet.sendTokens(1, 'eth', '' as Address)).rejects.toThrow( - 'Recipient address is required', - ) - }) - - it('should throw error for unsupported asset symbol', async () => { - await expect( - wallet.sendTokens(1, 'invalid-token', recipientAddress), - ).rejects.toThrow('Unsupported asset symbol: invalid-token') - }) - - it('should throw error for invalid token address', async () => { - await expect( - wallet.sendTokens(1, '0xinvalid', recipientAddress), - ).rejects.toThrow('Unknown asset address') - }) - }) - - describe('asset symbol case insensitivity', () => { - it('should handle uppercase ETH', async () => { - const transferData = await wallet.sendTokens(1, 'ETH', recipientAddress) - expect(transferData.to).toBe(recipientAddress) - expect(transferData.data).toBe('0x') - }) - - it('should handle mixed case USDC', async () => { - const usdcAddress = '0x078d782b760474a361dda0af3839290b0ef57ad6' - const transferData = await wallet.sendTokens(1, 'UsDc', recipientAddress) - expect(transferData.to).toBe(usdcAddress) - expect(transferData.value).toBe('0x0') - }) - }) -}) +// import { type Address, encodeFunctionData, erc20Abi, parseUnits } from 'viem' +// import { unichain } from 'viem/chains' +// import { beforeEach, describe, expect, it } from 'vitest' + +// import type { VerbsInterface } from '../types/verbs.js' +// import { initVerbs } from '../verbs.js' +// import { Wallet } from './index.js' + +// describe('Wallet SendTokens', () => { +// let verbs: VerbsInterface +// let wallet: Wallet +// const testAddress: Address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' +// const recipientAddress: Address = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' + +// beforeEach(() => { +// // Initialize Verbs SDK +// verbs = initVerbs({ +// wallet: { +// type: 'privy', +// appId: 'test-app-id', +// appSecret: 'test-app-secret', +// }, +// chains: [ +// { +// chainId: unichain.id, +// rpcUrl: 'http://localhost:9546', +// }, +// ], +// }) + +// // Create wallet instance +// wallet = new Wallet('test-wallet', verbs) +// wallet.init(testAddress) +// }) + +// describe('ETH transfers', () => { +// it('should create valid ETH transfer transaction data', async () => { +// const transferData = await wallet.sendTokens(1.5, 'eth', recipientAddress) + +// expect(transferData).toEqual({ +// to: recipientAddress, +// value: `0x${parseUnits('1.5', 18).toString(16)}`, +// data: '0x', +// }) +// }) + +// it('should handle fractional ETH amounts correctly', async () => { +// const transferData = await wallet.sendTokens( +// 0.001, +// 'eth', +// recipientAddress, +// ) +// const expectedValue = parseUnits('0.001', 18) + +// expect(transferData.to).toBe(recipientAddress) +// expect(transferData.value).toBe(`0x${expectedValue.toString(16)}`) +// expect(transferData.data).toBe('0x') +// }) + +// it('should handle large ETH amounts correctly', async () => { +// const transferData = await wallet.sendTokens(100, 'eth', recipientAddress) +// const expectedValue = parseUnits('100', 18) + +// expect(transferData.to).toBe(recipientAddress) +// expect(transferData.value).toBe(`0x${expectedValue.toString(16)}`) +// expect(transferData.data).toBe('0x') +// }) +// }) + +// describe('USDC transfers', () => { +// const usdcAddress = '0x078d782b760474a361dda0af3839290b0ef57ad6' // USDC on Unichain + +// it('should create valid USDC transfer transaction data', async () => { +// const transferData = await wallet.sendTokens( +// 100, +// 'usdc', +// recipientAddress, +// ) +// const expectedAmount = parseUnits('100', 6) // USDC has 6 decimals + +// const expectedData = encodeFunctionData({ +// abi: erc20Abi, +// functionName: 'transfer', +// args: [recipientAddress, expectedAmount], +// }) + +// expect(transferData).toEqual({ +// to: usdcAddress, +// value: '0x0', +// data: expectedData, +// }) +// }) + +// it('should handle fractional USDC amounts correctly', async () => { +// const transferData = await wallet.sendTokens( +// 0.5, +// 'usdc', +// recipientAddress, +// ) +// const expectedAmount = parseUnits('0.5', 6) // USDC has 6 decimals + +// const expectedData = encodeFunctionData({ +// abi: erc20Abi, +// functionName: 'transfer', +// args: [recipientAddress, expectedAmount], +// }) + +// expect(transferData.to).toBe(usdcAddress) +// expect(transferData.value).toBe('0x0') +// expect(transferData.data).toBe(expectedData) +// }) + +// it('should handle USDC by token address', async () => { +// const transferData = await wallet.sendTokens( +// 50, +// usdcAddress, +// recipientAddress, +// ) +// const expectedAmount = parseUnits('50', 6) // USDC has 6 decimals + +// const expectedData = encodeFunctionData({ +// abi: erc20Abi, +// functionName: 'transfer', +// args: [recipientAddress, expectedAmount], +// }) + +// expect(transferData.to).toBe(usdcAddress) +// expect(transferData.value).toBe('0x0') +// expect(transferData.data).toBe(expectedData) +// }) +// }) + +// describe('validation', () => { +// it('should throw error if wallet is not initialized', async () => { +// const uninitializedWallet = new Wallet('test-wallet', verbs) + +// await expect( +// uninitializedWallet.sendTokens(1, 'eth', recipientAddress), +// ).rejects.toThrow('Wallet not initialized') +// }) + +// it('should throw error for zero amount', async () => { +// await expect( +// wallet.sendTokens(0, 'eth', recipientAddress), +// ).rejects.toThrow('Amount must be greater than 0') +// }) + +// it('should throw error for negative amount', async () => { +// await expect( +// wallet.sendTokens(-1, 'eth', recipientAddress), +// ).rejects.toThrow('Amount must be greater than 0') +// }) + +// it('should throw error for empty recipient address', async () => { +// await expect(wallet.sendTokens(1, 'eth', '' as Address)).rejects.toThrow( +// 'Recipient address is required', +// ) +// }) + +// it('should throw error for unsupported asset symbol', async () => { +// await expect( +// wallet.sendTokens(1, 'invalid-token', recipientAddress), +// ).rejects.toThrow('Unsupported asset symbol: invalid-token') +// }) + +// it('should throw error for invalid token address', async () => { +// await expect( +// wallet.sendTokens(1, '0xinvalid', recipientAddress), +// ).rejects.toThrow('Unknown asset address') +// }) +// }) + +// describe('asset symbol case insensitivity', () => { +// it('should handle uppercase ETH', async () => { +// const transferData = await wallet.sendTokens(1, 'ETH', recipientAddress) +// expect(transferData.to).toBe(recipientAddress) +// expect(transferData.data).toBe('0x') +// }) + +// it('should handle mixed case USDC', async () => { +// const usdcAddress = '0x078d782b760474a361dda0af3839290b0ef57ad6' +// const transferData = await wallet.sendTokens(1, 'UsDc', recipientAddress) +// expect(transferData.to).toBe(usdcAddress) +// expect(transferData.value).toBe('0x0') +// }) +// }) +// }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca4bb402..62e56afa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: '@hono/node-server': specifier: ^1.14.0 version: 1.17.1(hono@4.8.5) + '@privy-io/server-auth': + specifier: ^1.31.1 + version: 1.31.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.0.5)) commander: specifier: ^13.1.0 version: 13.1.0 @@ -1551,9 +1554,15 @@ packages: resolution: {integrity: sha512-0eJBoQNmCSsWSWhzEVSU8WqPm7bgeN6VaAmqeXvjk8Ni0jM8nyTYjmRAqiCSs3mRzsnlQVchkGR6lsMTHkHKbw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} + '@privy-io/api-base@1.6.0': + resolution: {integrity: sha512-ftlqjFw0Ww7Xn6Ad/1kEUsXRfKqNdmJYKat4ryJl2uPh60QXXlPfnf4y17dDFHJlnVb7qY10cCvKVz5ev5gAeg==} + '@privy-io/public-api@2.39.2': resolution: {integrity: sha512-olT2xyrVdmgcxxy4g5v/u1qm6poDz3VNRuUcu45aiCP0DfAiseFVxKOKHpXP6xpA1m3z+45HL6lv2mJuYNP67w==} + '@privy-io/public-api@2.43.1': + resolution: {integrity: sha512-zhGBTghZiwnqdA4YvrXXM7fsz3fWUltSkxNdnQTqKGb/IfV8aZ14ryuWvD4v5oPJGtqVcwKRfdDmW8TMPGZHog==} + '@privy-io/server-auth@1.28.6': resolution: {integrity: sha512-Czx6LTMj81FymrSrrkoTaC5QEJhlBp94sWCyIBCS0SpQd76C7Y7KQffcMB+g70xjVGsD187Qc5iskHp6GfMJ7g==} peerDependencies: @@ -1565,6 +1574,17 @@ packages: viem: optional: true + '@privy-io/server-auth@1.31.1': + resolution: {integrity: sha512-w0DT0VZCPcXa/Mxqzo7fhXoInX5i4J5BgvzjNsdtMuovgR790kMx/9+K/rSlgtQ/25/B7oDjoIk/f8kd5Ps6mA==} + peerDependencies: + ethers: ^6 + viem: ^2.24.1 + peerDependenciesMeta: + ethers: + optional: true + viem: + optional: true + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -6010,6 +6030,10 @@ snapshots: dependencies: zod: 3.25.76 + '@privy-io/api-base@1.6.0': + dependencies: + zod: 3.25.76 + '@privy-io/public-api@2.39.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: '@privy-io/api-base': 1.5.2 @@ -6022,6 +6046,18 @@ snapshots: - typescript - utf-8-validate + '@privy-io/public-api@2.43.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': + dependencies: + '@privy-io/api-base': 1.6.0 + bs58: 5.0.0 + libphonenumber-js: 1.12.10 + viem: 2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + '@privy-io/server-auth@1.28.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.0.5))': dependencies: '@hpke/chacha20poly1305': 1.6.3 @@ -6046,6 +6082,31 @@ snapshots: - typescript - utf-8-validate + '@privy-io/server-auth@1.31.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.0.5))': + dependencies: + '@hpke/chacha20poly1305': 1.6.3 + '@hpke/core': 1.7.3 + '@noble/curves': 1.9.4 + '@noble/hashes': 1.8.0 + '@privy-io/public-api': 2.43.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@scure/base': 1.2.6 + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + canonicalize: 2.1.0 + dotenv: 16.6.1 + jose: 4.15.9 + node-fetch-native: 1.6.6 + redaxios: 0.5.1 + svix: 1.69.0 + ts-case-convert: 2.1.0 + type-fest: 3.13.1 + optionalDependencies: + viem: 2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.0.5) + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.45.1': From 54a59b0759b4bc0beb4e2164f625f79e8f6bad00 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 20 Aug 2025 16:50:07 -0700 Subject: [PATCH 02/39] new approach to smart wallets --- packages/demo/backend/src/config/verbs.ts | 6 +- packages/demo/backend/src/services/wallet.ts | 4 +- packages/sdk/src/index.ts | 2 +- packages/sdk/src/verbs.ts | 116 ++----- packages/sdk/src/wallet/PrivyWallet.ts | 129 ++++++++ .../src/wallet/{index.ts => SmartWallet.ts} | 3 +- packages/sdk/src/wallet/WalletNamespace.ts | 30 ++ packages/sdk/src/wallet/providers/privy.ts | 296 ++++++------------ .../sdk/src/wallet/providers/smartWallet.ts | 102 ++++++ 9 files changed, 395 insertions(+), 293 deletions(-) create mode 100644 packages/sdk/src/wallet/PrivyWallet.ts rename packages/sdk/src/wallet/{index.ts => SmartWallet.ts} (98%) create mode 100644 packages/sdk/src/wallet/WalletNamespace.ts create mode 100644 packages/sdk/src/wallet/providers/smartWallet.ts diff --git a/packages/demo/backend/src/config/verbs.ts b/packages/demo/backend/src/config/verbs.ts index db959a79..1f040d1a 100644 --- a/packages/demo/backend/src/config/verbs.ts +++ b/packages/demo/backend/src/config/verbs.ts @@ -1,13 +1,13 @@ import { initVerbs, + type Verbs, type VerbsConfig, - type VerbsInterface, } from '@eth-optimism/verbs-sdk' import { unichain } from 'viem/chains' import { env } from './env.js' -let verbsInstance: VerbsInterface +let verbsInstance: Verbs export function createVerbsConfig(): VerbsConfig { return { @@ -34,7 +34,7 @@ export function initializeVerbs(config?: VerbsConfig): void { verbsInstance = initVerbs(verbsConfig) } -export function getVerbs(): VerbsInterface { +export function getVerbs() { if (!verbsInstance) { throw new Error('Verbs SDK not initialized. Call initializeVerbs() first.') } diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index 3d47842f..fc2a45a0 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -38,7 +38,7 @@ export async function createWallet(): Promise<{ chainType: 'ethereum', }) const verbs = getVerbs() - const addresses = await verbs.createWallet([getAddress(wallet.address)]) + const addresses = await verbs.wallet.smartWallet!.createWallet([getAddress(wallet.address)]) return { privyAddress: wallet.address, smartWalletAddress: addresses[0].address } } @@ -52,7 +52,7 @@ export async function getWallet(userId: string): Promise<{ throw new Error('Wallet not found') } const verbs = getVerbs() - const wallet = await verbs.getWallet( + const wallet = await verbs.wallet.smartWallet!.getWallet( [getAddress(privyWallet.address)], ) return { privyWallet, wallet } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 6495d033..d0eff5b9 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -26,4 +26,4 @@ export type { WalletProvider, } from './types/index.js' export { initVerbs, Verbs } from './verbs.js' -export { Wallet } from './wallet/index.js' +export { SmartWallet } from './wallet/SmartWallet.js' diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index bb6e87f4..14f17621 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -1,34 +1,25 @@ -import { chainById } from '@eth-optimism/viem/chains' -import type { Address, Hash, PublicClient } from 'viem' -import { - createPublicClient, - createWalletClient, - encodeAbiParameters, - http, - parseEventLogs, -} from 'viem' -import { privateKeyToAccount } from 'viem/accounts' +import type { PublicClient } from 'viem' +import { createPublicClient, http } from 'viem' import { mainnet, unichain } from 'viem/chains' -import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' -import { smartWalletFactoryAddress } from '@/constants/addresses.js' import { LendProviderMorpho } from '@/lend/index.js' import { ChainManager } from '@/services/ChainManager.js' import type { LendProvider } from '@/types/lend.js' -import type { VerbsConfig, VerbsInterface } from '@/types/verbs.js' -import type { Wallet as WalletInterface } from '@/types/wallet.js' -import { Wallet } from '@/wallet/index.js' +import type { VerbsConfig } from '@/types/verbs.js' + +import { WalletNamespace } from './wallet/WalletNamespace.js' /** * Main Verbs SDK class * @description Core implementation of the Verbs SDK */ -export class Verbs implements VerbsInterface { +export class Verbs { + public readonly wallet: WalletNamespace private _chainManager: ChainManager private lendProvider?: LendProvider - private privateKey?: Hash constructor(config: VerbsConfig) { + this.wallet = new WalletNamespace() this._chainManager = new ChainManager( config.chains || [ { @@ -37,7 +28,6 @@ export class Verbs implements VerbsInterface { }, ], ) - this.privateKey = config.privateKey // Create lending provider if configured if (config.lend) { // TODO: delete this code and just have the lend use the ChainManager @@ -60,76 +50,6 @@ export class Verbs implements VerbsInterface { } } - async createWallet( - ownerAddresses: Address[], - nonce?: bigint, - ): Promise> { - // deploy the wallet on each chain in the chain manager - const deployments = await Promise.all( - this._chainManager.getSupportedChains().map(async (chainId) => { - const walletClient = createWalletClient({ - chain: chainById[chainId], - transport: http(this._chainManager.getRpcUrl(chainId)), - account: privateKeyToAccount(this.privateKey!), - }) - const encodedOwners = ownerAddresses.map((ownerAddress) => - encodeAbiParameters([{ type: 'address' }], [ownerAddress]), - ) - const tx = await walletClient.writeContract({ - abi: smartWalletFactoryAbi, - address: smartWalletFactoryAddress, - functionName: 'createAccount', - args: [encodedOwners, nonce || 0n], - }) - const publicClient = this._chainManager.getPublicClient(chainId) - const receipt = await publicClient.waitForTransactionReceipt({ - hash: tx, - }) - if (!receipt.status) { - throw new Error('Wallet deployment failed') - } - // parse logs - const logs = parseEventLogs({ - abi: smartWalletFactoryAbi, - eventName: 'AccountCreated', - logs: receipt.logs, - }) - return { - chainId, - address: logs[0].args.account, - } - }), - ) - return deployments - } - - async getWallet( - initialOwnerAddresses: Address[], - nonce?: bigint, - currentOwnerAddresses?: Address[], - ): Promise { - // Factory is the same accross all chains, so we can use the first chain to get the wallet address - const publicClient = this._chainManager.getPublicClient( - this._chainManager.getSupportedChains()[0], - ) - const encodedOwners = initialOwnerAddresses.map((ownerAddress) => - encodeAbiParameters([{ type: 'address' }], [ownerAddress]), - ) - const smartWalletAddress = await publicClient.readContract({ - abi: smartWalletFactoryAbi, - address: smartWalletFactoryAddress, - functionName: 'getAddress', - args: [encodedOwners, nonce || 0n], - }) - const owners = currentOwnerAddresses || initialOwnerAddresses - return new Wallet( - smartWalletAddress, - owners, - this._chainManager, - this.lendProvider!, - ) - } - /** * Get the lend provider instance * @returns LendProvider instance if configured, undefined otherwise @@ -156,6 +76,22 @@ export class Verbs implements VerbsInterface { * @param config - SDK configuration * @returns Initialized Verbs SDK instance */ -export function initVerbs(config: VerbsConfig): VerbsInterface { - return new Verbs(config) +export function initVerbs(config: VerbsConfig) { + const verbs = new Verbs(config) + if (config.wallet) { + verbs.wallet.withPrivy( + config.wallet.appId, + config.wallet.appSecret, + verbs.chainManager, + ) + } + if (config.privateKey) { + verbs.wallet.withSmartWallet( + verbs.chainManager, + config.privateKey, + verbs.lend, + ) + } + + return verbs } diff --git a/packages/sdk/src/wallet/PrivyWallet.ts b/packages/sdk/src/wallet/PrivyWallet.ts new file mode 100644 index 00000000..0971ca2c --- /dev/null +++ b/packages/sdk/src/wallet/PrivyWallet.ts @@ -0,0 +1,129 @@ +import type { Address, Hash } from 'viem' + +import type { ChainManager } from '@/services/ChainManager.js' +import type { TransactionData } from '@/types/lend.js' + +import type { PrivyWalletProvider } from './providers/privy.js' + +/** + * Privy wallet implementation + * @description Wallet implementation using Privy service + */ +export class PrivyWallet { + private privyProvider: PrivyWalletProvider + private chainManager: ChainManager + private walletId: string + private address: Address + /** + * Create a new Privy wallet provider + * @param appId - Privy application ID + * @param appSecret - Privy application secret + * @param verbs - Verbs instance for accessing configured providers + */ + constructor( + privyProvider: PrivyWalletProvider, + chainManager: ChainManager, + walletId: string, + address: Address, + ) { + this.privyProvider = privyProvider + this.chainManager = chainManager + this.walletId = walletId + this.address = address + } + + /** + * Sign and send a transaction using Privy + * @description Signs and sends a transaction using Privy's wallet API + * @param walletId - Wallet ID to use for signing + * @param transactionData - Transaction data to sign and send + * @returns Promise resolving to transaction hash + * @throws Error if transaction signing fails + */ + async sign(transactionData: TransactionData): Promise { + try { + const response = + await this.privyProvider.privy.walletApi.ethereum.sendTransaction({ + walletId: this.walletId, + caip2: 'eip155:130', // Unichain + transaction: { + to: transactionData.to, + data: transactionData.data as `0x${string}`, + value: Number(transactionData.value), + chainId: 130, // Unichain + }, + }) + + return response.hash as Hash + } catch (error) { + throw new Error( + `Failed to sign transaction for wallet ${this.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 + * @param walletId - Wallet ID to use for signing + * @param transactionData - Transaction data to sign + * @returns Promise resolving to signed transaction + * @throws Error if transaction signing fails + */ + async signOnly(transactionData: TransactionData): Promise { + try { + // Get public client for gas estimation + const publicClient = this.chainManager.getPublicClient(130) // Unichain + + // Estimate gas limit + const gasLimit = await publicClient.estimateGas({ + account: this.address, + to: transactionData.to, + data: transactionData.data as `0x${string}`, + value: BigInt(transactionData.value), + }) + + // Get current gas price and fee data + const feeData = await publicClient.estimateFeesPerGas() + + // Get current nonce for the wallet - manual management since Privy isn't handling it properly + const nonce = await publicClient.getTransactionCount({ + address: this.address, + blockTag: 'pending', // Use pending to get the next nonce including any pending txs + }) + + // According to Privy docs: if you provide ANY gas parameters, you must provide ALL of them + const txParams: any = { + to: transactionData.to, + data: transactionData.data as `0x${string}`, + value: transactionData.value as `0x${string}`, + chainId: 130, // Unichain + type: 2, // EIP-1559 + gasLimit: `0x${gasLimit.toString(16)}`, + maxFeePerGas: `0x${(feeData.maxFeePerGas || BigInt(1000000000)).toString(16)}`, // fallback to 1 gwei + maxPriorityFeePerGas: `0x${(feeData.maxPriorityFeePerGas || BigInt(100000000)).toString(16)}`, // fallback to 0.1 gwei + 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.privyProvider.privy.walletApi.ethereum.signTransaction({ + walletId: this.walletId, + transaction: txParams, + }) + + return response.signedTransaction + } catch (error) { + throw new Error( + `Failed to sign transaction for wallet ${this.walletId}: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + ) + } + } +} diff --git a/packages/sdk/src/wallet/index.ts b/packages/sdk/src/wallet/SmartWallet.ts similarity index 98% rename from packages/sdk/src/wallet/index.ts rename to packages/sdk/src/wallet/SmartWallet.ts index b09e5da5..cd098bac 100644 --- a/packages/sdk/src/wallet/index.ts +++ b/packages/sdk/src/wallet/SmartWallet.ts @@ -14,7 +14,6 @@ import type { TransactionData, } from '@/types/lend.js' import type { TokenBalance } from '@/types/token.js' -import type { Wallet as WalletInterface } from '@/types/wallet.js' import { type AssetIdentifier, parseAssetAmount, @@ -26,7 +25,7 @@ import { * Wallet implementation * @description Concrete implementation of the Wallet interface */ -export class Wallet implements WalletInterface { +export class SmartWallet { public ownerAddresses: Address[] public address: Address private lendProvider: LendProvider diff --git a/packages/sdk/src/wallet/WalletNamespace.ts b/packages/sdk/src/wallet/WalletNamespace.ts new file mode 100644 index 00000000..3c894021 --- /dev/null +++ b/packages/sdk/src/wallet/WalletNamespace.ts @@ -0,0 +1,30 @@ +import type { Hash } from 'viem' + +import type { ChainManager } from '@/services/ChainManager.js' +import type { LendProvider } from '@/types/lend.js' +import { PrivyWalletProvider } from '@/wallet/providers/privy.js' +import { SmartWalletProvider } from '@/wallet/providers/smartWallet.js' + +// Wallet namespace that holds all providers +export class WalletNamespace { + public privy?: PrivyWalletProvider + public smartWallet?: SmartWalletProvider + + withPrivy(appId: string, appSecret: string, chainManager: ChainManager) { + this.privy = new PrivyWalletProvider(appId, appSecret, chainManager) + return this + } + + withSmartWallet( + chainManager: ChainManager, + deployerPrivateKey: Hash, + lendProvider: LendProvider, + ) { + this.smartWallet = new SmartWalletProvider( + chainManager, + deployerPrivateKey, + lendProvider, + ) + return this + } +} diff --git a/packages/sdk/src/wallet/providers/privy.ts b/packages/sdk/src/wallet/providers/privy.ts index ce6a5b32..ec5f5ce1 100644 --- a/packages/sdk/src/wallet/providers/privy.ts +++ b/packages/sdk/src/wallet/providers/privy.ts @@ -1,195 +1,101 @@ -// import { PrivyClient } from '@privy-io/server-auth' -// import { getAddress, type Hash } from 'viem' - -// import { Wallet } from '@/index.js' -// import type { TransactionData } from '@/types/lend.js' -// import type { VerbsInterface } from '@/types/verbs.js' -// import type { GetAllWalletsOptions, WalletProvider } from '@/types/wallet.js' - -// /** -// * Privy wallet provider implementation -// * @description Wallet provider implementation using Privy service -// */ -// export class WalletProviderPrivy implements WalletProvider { -// private privy: PrivyClient -// private verbs: VerbsInterface - -// /** -// * Create a new Privy wallet provider -// * @param appId - Privy application ID -// * @param appSecret - Privy application secret -// * @param verbs - Verbs instance for accessing configured providers -// */ -// constructor(appId: string, appSecret: string, verbs: VerbsInterface) { -// this.privy = new PrivyClient(appId, appSecret) -// this.verbs = verbs -// } - -// /** -// * Create new wallet via Privy -// * @description Creates a new wallet using Privy's wallet API -// * @param userId - User identifier for the wallet -// * @returns Promise resolving to new wallet instance -// * @throws Error if wallet creation fails -// */ -// async createWallet(userId: string): Promise { -// try { -// const wallet = await this.privy.walletApi.createWallet({ -// chainType: 'ethereum', -// }) - -// const walletInstance = new Wallet(wallet.id, this.verbs, this) -// walletInstance.init(getAddress(wallet.address)) -// return walletInstance -// } catch { -// throw new Error(`Failed to create wallet for user ${userId}`) -// } -// } - -// /** -// * Get wallet by user ID via Privy -// * @description Retrieves wallet information from Privy service -// * @param userId - User identifier -// * @returns Promise resolving to wallet or null if not found -// */ -// async getWallet(userId: string): Promise { -// try { -// // TODO: Implement proper user-to-wallet lookup -// const wallet = await this.privy.walletApi.getWallet({ id: userId }) - -// const walletInstance = new Wallet(wallet.id, this.verbs.chainManager, this.verbs.lendProvider!) -// walletInstance.init(getAddress(wallet.address)) -// return walletInstance -// } catch { -// return null -// } -// } - -// /** -// * Get all wallets via Privy -// * @description Retrieves all wallets from Privy service with optional filtering -// * @param options - Optional parameters for filtering and pagination -// * @returns Promise resolving to array of wallets -// */ -// async getAllWallets(options?: GetAllWalletsOptions): Promise { -// try { -// const response = await this.privy.walletApi.getWallets({ -// limit: options?.limit, -// cursor: options?.cursor, -// }) - -// return response.data.map((wallet) => { -// const walletInstance = new Wallet(wallet.id, this.verbs, this) -// walletInstance.init(getAddress(wallet.address)) -// return walletInstance -// }) -// } catch { -// throw new Error('Failed to retrieve wallets') -// } -// } - -// /** -// * Sign and send a transaction using Privy -// * @description Signs and sends a transaction using Privy's wallet API -// * @param walletId - Wallet ID to use for signing -// * @param transactionData - Transaction data to sign and send -// * @returns Promise resolving to transaction hash -// * @throws Error if transaction signing fails -// */ -// async sign( -// walletId: string, -// transactionData: TransactionData, -// ): Promise { -// try { -// const response = await this.privy.walletApi.ethereum.sendTransaction({ -// walletId, -// caip2: 'eip155:130', // Unichain -// transaction: { -// to: transactionData.to, -// data: transactionData.data as `0x${string}`, -// value: Number(transactionData.value), -// chainId: 130, // Unichain -// }, -// }) - -// return response.hash as Hash -// } catch (error) { -// throw new Error( -// `Failed to sign 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 -// * @param walletId - Wallet ID to use for signing -// * @param transactionData - Transaction data to sign -// * @returns Promise resolving to signed transaction -// * @throws Error if transaction signing fails -// */ -// async signOnly( -// walletId: string, -// transactionData: TransactionData, -// ): Promise { -// try { -// // Get wallet to determine the from address for gas estimation -// const wallet = await this.getWallet(walletId) -// if (!wallet) { -// throw new Error(`Wallet not found: ${walletId}`) -// } - -// // Get public client for gas estimation -// const publicClient = this.verbs.chainManager.getPublicClient(130) // Unichain - -// // Estimate gas limit -// const gasLimit = await publicClient.estimateGas({ -// account: wallet.address, -// to: transactionData.to, -// data: transactionData.data as `0x${string}`, -// value: BigInt(transactionData.value), -// }) - -// // Get current gas price and fee data -// const feeData = await publicClient.estimateFeesPerGas() - -// // Get current nonce for the wallet - manual management since Privy isn't handling it properly -// const nonce = await publicClient.getTransactionCount({ -// address: wallet.address, -// blockTag: 'pending', // Use pending to get the next nonce including any pending txs -// }) - -// // According to Privy docs: if you provide ANY gas parameters, you must provide ALL of them -// const txParams: any = { -// to: transactionData.to, -// data: transactionData.data as `0x${string}`, -// value: transactionData.value as `0x${string}`, -// chainId: 130, // Unichain -// type: 2, // EIP-1559 -// gasLimit: `0x${gasLimit.toString(16)}`, -// maxFeePerGas: `0x${(feeData.maxFeePerGas || BigInt(1000000000)).toString(16)}`, // fallback to 1 gwei -// maxPriorityFeePerGas: `0x${(feeData.maxPriorityFeePerGas || BigInt(100000000)).toString(16)}`, // fallback to 0.1 gwei -// 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, -// transaction: txParams, -// }) - -// return response.signedTransaction -// } catch (error) { -// throw new Error( -// `Failed to sign transaction for wallet ${walletId}: ${ -// error instanceof Error ? error.message : 'Unknown error' -// }`, -// ) -// } -// } -// } +import { PrivyClient } from '@privy-io/server-auth' +import { getAddress } from 'viem' + +import type { ChainManager } from '@/services/ChainManager.js' +import type { GetAllWalletsOptions } from '@/types/wallet.js' +import { PrivyWallet } from '@/wallet/PrivyWallet.js' + +/** + * Privy wallet provider implementation + * @description Wallet provider implementation using Privy service + */ +export class PrivyWalletProvider { + public privy: PrivyClient + private chainManager: ChainManager + + /** + * Create a new Privy wallet provider + * @param appId - Privy application ID + * @param appSecret - Privy application secret + * @param verbs - Verbs instance for accessing configured providers + */ + constructor(appId: string, appSecret: string, chainManager: ChainManager) { + this.privy = new PrivyClient(appId, appSecret) + this.chainManager = chainManager + } + + /** + * Create new wallet via Privy + * @description Creates a new wallet using Privy's wallet API + * @param userId - User identifier for the wallet + * @returns Promise resolving to new wallet instance + * @throws Error if wallet creation fails + */ + async createWallet(userId: string): Promise { + try { + const wallet = await this.privy.walletApi.createWallet({ + chainType: 'ethereum', + }) + + const walletInstance = new PrivyWallet( + this, + this.chainManager, + wallet.id, + getAddress(wallet.address), + ) + return walletInstance + } catch { + throw new Error(`Failed to create wallet for user ${userId}`) + } + } + + /** + * Get wallet by user ID via Privy + * @description Retrieves wallet information from Privy service + * @param userId - User identifier + * @returns Promise resolving to wallet or null if not found + */ + async getWallet(userId: string): Promise { + try { + // TODO: Implement proper user-to-wallet lookup + const wallet = await this.privy.walletApi.getWallet({ id: userId }) + + const walletInstance = new PrivyWallet( + this, + this.chainManager, + wallet.id, + getAddress(wallet.address), + ) + return walletInstance + } catch { + return null + } + } + + /** + * Get all wallets via Privy + * @description Retrieves all wallets from Privy service with optional filtering + * @param options - Optional parameters for filtering and pagination + * @returns Promise resolving to array of wallets + */ + async getAllWallets(options?: GetAllWalletsOptions): Promise { + try { + const response = await this.privy.walletApi.getWallets({ + limit: options?.limit, + cursor: options?.cursor, + }) + + return response.data.map((wallet) => { + const walletInstance = new PrivyWallet( + this, + this.chainManager, + wallet.id, + getAddress(wallet.address), + ) + return walletInstance + }) + } catch { + throw new Error('Failed to retrieve wallets') + } + } +} diff --git a/packages/sdk/src/wallet/providers/smartWallet.ts b/packages/sdk/src/wallet/providers/smartWallet.ts new file mode 100644 index 00000000..bd461d8d --- /dev/null +++ b/packages/sdk/src/wallet/providers/smartWallet.ts @@ -0,0 +1,102 @@ +import { chainById } from '@eth-optimism/viem/chains' +import type { Address, Hash } from 'viem' +import { + createWalletClient, + encodeAbiParameters, + http, + parseEventLogs, +} from 'viem' +import { privateKeyToAccount } from 'viem/accounts' + +import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' +import { smartWalletFactoryAddress } from '@/constants/addresses.js' +import type { WalletInterface } from '@/index.js' +import type { ChainManager } from '@/services/ChainManager.js' +import type { LendProvider } from '@/types/lend.js' +import { SmartWallet } from '@/wallet/SmartWallet.js' + +export class SmartWalletProvider { + private chainManager: ChainManager + private deployerPrivateKey: Hash + private lendProvider: LendProvider + + constructor( + chainManager: ChainManager, + deployerPrivateKey: Hash, + lendProvider: LendProvider, + ) { + this.chainManager = chainManager + this.deployerPrivateKey = deployerPrivateKey + this.lendProvider = lendProvider + } + + async createWallet( + ownerAddresses: Address[], + nonce?: bigint, + ): Promise> { + // deploy the wallet on each chain in the chain manager + const deployments = await Promise.all( + this.chainManager.getSupportedChains().map(async (chainId) => { + const walletClient = createWalletClient({ + chain: chainById[chainId], + transport: http(this.chainManager.getRpcUrl(chainId)), + account: privateKeyToAccount(this.deployerPrivateKey), + }) + const encodedOwners = ownerAddresses.map((ownerAddress) => + encodeAbiParameters([{ type: 'address' }], [ownerAddress]), + ) + const tx = await walletClient.writeContract({ + abi: smartWalletFactoryAbi, + address: smartWalletFactoryAddress, + functionName: 'createAccount', + args: [encodedOwners, nonce || 0n], + }) + const publicClient = this.chainManager.getPublicClient(chainId) + const receipt = await publicClient.waitForTransactionReceipt({ + hash: tx, + }) + if (!receipt.status) { + throw new Error('Wallet deployment failed') + } + // parse logs + const logs = parseEventLogs({ + abi: smartWalletFactoryAbi, + eventName: 'AccountCreated', + logs: receipt.logs, + }) + return { + chainId, + address: logs[0].args.account, + } + }), + ) + return deployments + } + + async getWallet( + initialOwnerAddresses: Address[], + nonce?: bigint, + currentOwnerAddresses?: Address[], + ): Promise { + // Factory is the same accross all chains, so we can use the first chain to get the wallet address + const publicClient = this.chainManager.getPublicClient( + this.chainManager.getSupportedChains()[0], + ) + const encodedOwners = initialOwnerAddresses.map((ownerAddress) => + encodeAbiParameters([{ type: 'address' }], [ownerAddress]), + ) + const smartWalletAddress = await publicClient.readContract({ + abi: smartWalletFactoryAbi, + address: smartWalletFactoryAddress, + functionName: 'getAddress', + args: [encodedOwners, nonce || 0n], + }) + const owners = currentOwnerAddresses || initialOwnerAddresses + return new SmartWallet( + smartWalletAddress, + owners, + this.chainManager, + this.lendProvider, + ) + } +} From 3f6a46545c492dc4f5598588827d7a4669ca20f8 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 20 Aug 2025 17:16:55 -0700 Subject: [PATCH 03/39] refactored privy and lend is working --- .../demo/backend/src/controllers/wallet.ts | 2 +- packages/demo/backend/src/services/lend.ts | 22 ++--- packages/demo/backend/src/services/wallet.ts | 56 +++++------ packages/sdk/src/index.ts | 1 + packages/sdk/src/wallet/PrivyWallet.ts | 99 ++++++------------- packages/sdk/src/wallet/providers/privy.ts | 5 +- .../sdk/src/wallet/providers/smartWallet.ts | 12 ++- 7 files changed, 75 insertions(+), 122 deletions(-) diff --git a/packages/demo/backend/src/controllers/wallet.ts b/packages/demo/backend/src/controllers/wallet.ts index 1a2f9dc4..1d1e3454 100644 --- a/packages/demo/backend/src/controllers/wallet.ts +++ b/packages/demo/backend/src/controllers/wallet.ts @@ -132,7 +132,7 @@ export class WalletController { return c.json({ wallets: wallets.map(({ privyWallet }) => ({ address: privyWallet.address as Address, - id: privyWallet.id, + id: privyWallet.walletId, })), count: wallets.length, } satisfies GetAllWalletsResponse) diff --git a/packages/demo/backend/src/services/lend.ts b/packages/demo/backend/src/services/lend.ts index 23096910..1273b57c 100644 --- a/packages/demo/backend/src/services/lend.ts +++ b/packages/demo/backend/src/services/lend.ts @@ -1,11 +1,9 @@ import type { LendTransaction, LendVaultInfo, + SmartWallet, SupportedChainId, - WalletInterface, } from '@eth-optimism/verbs-sdk' -import { PrivyClient } from '@privy-io/server-auth' -import { env } from 'process' import type { Address, PublicClient } from 'viem' import { getVerbs } from '../config/verbs.js' @@ -109,7 +107,7 @@ export async function executeLendTransaction( lendTransaction: LendTransaction, ): Promise { const verbs = getVerbs() - const { wallet } = await getWallet(walletId) + const { wallet, privyWallet } = await getWallet(walletId) if (!wallet) { throw new Error(`Wallet not found for user ID: ${walletId}`) @@ -121,7 +119,7 @@ export async function executeLendTransaction( const publicClient = verbs.chainManager.getPublicClient(130) const ethBalance = await publicClient.getBalance({ - address: wallet.ownerAddresses[0], + address: privyWallet.address, }) const gasEstimate = await estimateGasCost( @@ -161,7 +159,7 @@ async function signOnly( ): Promise { try { // Get wallet to determine the from address for gas estimation - const { wallet } = await getWallet(walletId) + const { wallet, privyWallet } = await getWallet(walletId) if (!wallet) { throw new Error(`Wallet not found: ${walletId}`) } @@ -171,13 +169,9 @@ async function signOnly( `[PRIVY_PROVIDER] Complete tx params - Type: ${txParams.type}, Nonce: ${txParams.nonce}, Limit: ${txParams.gasLimit}, MaxFee: ${txParams.maxFeePerGas || 'fallback'}, Priority: ${txParams.maxPriorityFeePerGas || 'fallback'}`, ) - const privy = new PrivyClient(env.PRIVY_APP_ID!, env.PRIVY_APP_SECRET!) - const response = await privy.walletApi.ethereum.signTransaction({ - walletId, - transaction: txParams, - }) - console.log('Signed transaction', response.signedTransaction) - return response.signedTransaction + const signedTransaction = await privyWallet.signOnly(txParams) + console.log('Signed transaction', signedTransaction) + return signedTransaction } catch (error) { console.error('Error signing transaction', error) throw new Error( @@ -190,7 +184,7 @@ async function signOnly( async function estimateGasCost( publicClient: PublicClient, - wallet: WalletInterface, + wallet: SmartWallet, lendTransaction: LendTransaction, ): Promise { let totalGasEstimate = BigInt(0) diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index fc2a45a0..5ed924e1 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -1,14 +1,11 @@ import type { GetAllWalletsOptions, + PrivyWallet, + SmartWallet, TokenBalance, TransactionData, - WalletInterface, } from '@eth-optimism/verbs-sdk' import { unichain } from '@eth-optimism/viem/chains' -import { - PrivyClient, - type WalletApiWalletResponseType, -} from '@privy-io/server-auth' import type { Address, Hex } from 'viem' import { createPublicClient, @@ -30,29 +27,28 @@ export async function createWallet(): Promise<{ privyAddress: string smartWalletAddress: string }> { - /** - * - */ - const privy = new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET) - const wallet = await privy.walletApi.createWallet({ - chainType: 'ethereum', - }) const verbs = getVerbs() - const addresses = await verbs.wallet.smartWallet!.createWallet([getAddress(wallet.address)]) - return { privyAddress: wallet.address, smartWalletAddress: addresses[0].address } + const privyWallet = await verbs.wallet.privy!.createWallet() + const smartWallet = await verbs.wallet.smartWallet!.createWallet([getAddress(privyWallet.address)]) + return { privyAddress: privyWallet.address, smartWalletAddress: smartWallet.address } } export async function getWallet(userId: string): Promise<{ - privyWallet: WalletApiWalletResponseType - wallet: WalletInterface + privyWallet: PrivyWallet + wallet: SmartWallet }> { - const privy = new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET) - const privyWallet = await privy.walletApi.getWallet({ id: userId }) + const verbs = getVerbs() + if (!verbs.wallet.privy) { + throw new Error('Privy wallet not configured') + } + if (!verbs.wallet.smartWallet) { + throw new Error('Smart wallet not configured') + } + const privyWallet = await verbs.wallet.privy.getWallet(userId) if (!privyWallet) { throw new Error('Wallet not found') } - const verbs = getVerbs() - const wallet = await verbs.wallet.smartWallet!.getWallet( + const wallet = await verbs.wallet.smartWallet.getWallet( [getAddress(privyWallet.address)], ) return { privyWallet, wallet } @@ -61,18 +57,20 @@ export async function getWallet(userId: string): Promise<{ export async function getAllWallets( options?: GetAllWalletsOptions, ): Promise< - Array<{ privyWallet: WalletApiWalletResponseType; wallet: WalletInterface }> + Array<{ privyWallet: PrivyWallet; wallet: SmartWallet }> > { try { - const privy = new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET) - const response = await privy.walletApi.getWallets({ - limit: options?.limit, - cursor: options?.cursor, - }) - + const verbs = getVerbs() + if (!verbs.wallet.privy) { + throw new Error('Privy wallet not configured') + } + const privyWallets = await verbs.wallet.privy.getAllWallets(options) return Promise.all( - response.data.map((wallet) => { - return getWallet(wallet.id) + privyWallets.map(async (wallet) => { + if (!verbs.wallet.smartWallet) { + throw new Error('Smart wallet not configured') + } + return { privyWallet: wallet, wallet: await verbs.wallet.smartWallet.getWallet([getAddress(wallet.address)]) } }), ) } catch { diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index d0eff5b9..eeec71d2 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -26,4 +26,5 @@ export type { WalletProvider, } from './types/index.js' export { initVerbs, Verbs } from './verbs.js' +export { PrivyWallet } from './wallet/PrivyWallet.js' export { SmartWallet } from './wallet/SmartWallet.js' diff --git a/packages/sdk/src/wallet/PrivyWallet.ts b/packages/sdk/src/wallet/PrivyWallet.ts index 0971ca2c..4265b125 100644 --- a/packages/sdk/src/wallet/PrivyWallet.ts +++ b/packages/sdk/src/wallet/PrivyWallet.ts @@ -1,7 +1,6 @@ -import type { Address, Hash } from 'viem' +import type { Address, Hex, Quantity } from 'viem' import type { ChainManager } from '@/services/ChainManager.js' -import type { TransactionData } from '@/types/lend.js' import type { PrivyWalletProvider } from './providers/privy.js' @@ -10,10 +9,10 @@ import type { PrivyWalletProvider } from './providers/privy.js' * @description Wallet implementation using Privy service */ export class PrivyWallet { + public address: Address + public walletId: string private privyProvider: PrivyWalletProvider private chainManager: ChainManager - private walletId: string - private address: Address /** * Create a new Privy wallet provider * @param appId - Privy application ID @@ -32,38 +31,6 @@ export class PrivyWallet { this.address = address } - /** - * Sign and send a transaction using Privy - * @description Signs and sends a transaction using Privy's wallet API - * @param walletId - Wallet ID to use for signing - * @param transactionData - Transaction data to sign and send - * @returns Promise resolving to transaction hash - * @throws Error if transaction signing fails - */ - async sign(transactionData: TransactionData): Promise { - try { - const response = - await this.privyProvider.privy.walletApi.ethereum.sendTransaction({ - walletId: this.walletId, - caip2: 'eip155:130', // Unichain - transaction: { - to: transactionData.to, - data: transactionData.data as `0x${string}`, - value: Number(transactionData.value), - chainId: 130, // Unichain - }, - }) - - return response.hash as Hash - } catch (error) { - throw new Error( - `Failed to sign transaction for wallet ${this.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 @@ -72,43 +39,33 @@ export class PrivyWallet { * @returns Promise resolving to signed transaction * @throws Error if transaction signing fails */ - async signOnly(transactionData: TransactionData): Promise { + async signOnly(txParams: { + /** The address the transaction is sent from. Must be hexadecimal formatted. */ + from?: Hex + /** Destination address of the transaction. */ + to?: Hex + /** The nonce to be used for the transaction (hexadecimal or number). */ + nonce?: Quantity + /** (optional) The chain ID of network your transaction will be sent on. */ + chainId?: Quantity + /** (optional) Data to send to the receiving address, especially when calling smart contracts. Must be hexadecimal formatted. */ + data?: Hex + /** (optional) The value (in wei) be sent with the transaction (hexadecimal or number). */ + value?: Quantity + /** (optional) The EIP-2718 transction type (e.g. `2` for EIP-1559 transactions). */ + type?: 0 | 1 | 2 + /** (optional) The max units of gas that can be used by this transaction (hexadecimal or number). */ + gasLimit?: Quantity + /** (optional) The price (in wei) per unit of gas for this transaction (hexadecimal or number), for use in non EIP-1559 transactions (type 0 or 1). */ + gasPrice?: Quantity + /** (optional) The maxFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ + maxFeePerGas?: Quantity + /** (optional) The maxPriorityFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ + maxPriorityFeePerGas?: Quantity + }): Promise { try { - // Get public client for gas estimation - const publicClient = this.chainManager.getPublicClient(130) // Unichain - - // Estimate gas limit - const gasLimit = await publicClient.estimateGas({ - account: this.address, - to: transactionData.to, - data: transactionData.data as `0x${string}`, - value: BigInt(transactionData.value), - }) - - // Get current gas price and fee data - const feeData = await publicClient.estimateFeesPerGas() - - // Get current nonce for the wallet - manual management since Privy isn't handling it properly - const nonce = await publicClient.getTransactionCount({ - address: this.address, - blockTag: 'pending', // Use pending to get the next nonce including any pending txs - }) - - // According to Privy docs: if you provide ANY gas parameters, you must provide ALL of them - const txParams: any = { - to: transactionData.to, - data: transactionData.data as `0x${string}`, - value: transactionData.value as `0x${string}`, - chainId: 130, // Unichain - type: 2, // EIP-1559 - gasLimit: `0x${gasLimit.toString(16)}`, - maxFeePerGas: `0x${(feeData.maxFeePerGas || BigInt(1000000000)).toString(16)}`, // fallback to 1 gwei - maxPriorityFeePerGas: `0x${(feeData.maxPriorityFeePerGas || BigInt(100000000)).toString(16)}`, // fallback to 0.1 gwei - 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'}`, + `[PRIVY_PROVIDER] Complete tx params - Type: ${txParams.type}, Nonce: ${txParams.nonce}, Limit: ${txParams.gasLimit}, MaxFee: ${txParams.maxFeePerGas || 'fallback'}, Priority: ${txParams.maxPriorityFeePerGas || 'fallback'}`, ) const response = diff --git a/packages/sdk/src/wallet/providers/privy.ts b/packages/sdk/src/wallet/providers/privy.ts index ec5f5ce1..e2bae1e2 100644 --- a/packages/sdk/src/wallet/providers/privy.ts +++ b/packages/sdk/src/wallet/providers/privy.ts @@ -27,11 +27,10 @@ export class PrivyWalletProvider { /** * Create new wallet via Privy * @description Creates a new wallet using Privy's wallet API - * @param userId - User identifier for the wallet * @returns Promise resolving to new wallet instance * @throws Error if wallet creation fails */ - async createWallet(userId: string): Promise { + async createWallet(): Promise { try { const wallet = await this.privy.walletApi.createWallet({ chainType: 'ethereum', @@ -45,7 +44,7 @@ export class PrivyWalletProvider { ) return walletInstance } catch { - throw new Error(`Failed to create wallet for user ${userId}`) + throw new Error(`Failed to create wallet`) } } diff --git a/packages/sdk/src/wallet/providers/smartWallet.ts b/packages/sdk/src/wallet/providers/smartWallet.ts index bd461d8d..2390089a 100644 --- a/packages/sdk/src/wallet/providers/smartWallet.ts +++ b/packages/sdk/src/wallet/providers/smartWallet.ts @@ -10,7 +10,6 @@ import { privateKeyToAccount } from 'viem/accounts' import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' import { smartWalletFactoryAddress } from '@/constants/addresses.js' -import type { WalletInterface } from '@/index.js' import type { ChainManager } from '@/services/ChainManager.js' import type { LendProvider } from '@/types/lend.js' import { SmartWallet } from '@/wallet/SmartWallet.js' @@ -33,7 +32,7 @@ export class SmartWalletProvider { async createWallet( ownerAddresses: Address[], nonce?: bigint, - ): Promise> { + ): Promise { // deploy the wallet on each chain in the chain manager const deployments = await Promise.all( this.chainManager.getSupportedChains().map(async (chainId) => { @@ -70,14 +69,19 @@ export class SmartWalletProvider { } }), ) - return deployments + return new SmartWallet( + deployments[0].address, + ownerAddresses, + this.chainManager, + this.lendProvider, + ) } async getWallet( initialOwnerAddresses: Address[], nonce?: bigint, currentOwnerAddresses?: Address[], - ): Promise { + ): Promise { // Factory is the same accross all chains, so we can use the first chain to get the wallet address const publicClient = this.chainManager.getPublicClient( this.chainManager.getSupportedChains()[0], From 275bd384b3baa9b34daef27f193f61e2b52440ae Mon Sep 17 00:00:00 2001 From: tre Date: Fri, 22 Aug 2025 18:25:44 -0700 Subject: [PATCH 04/39] spike: paymaster with smart accounts on base sepolia --- packages/demo/backend/src/config/env.ts | 1 + packages/demo/backend/src/config/verbs.ts | 7 +- packages/demo/backend/src/controllers/lend.ts | 23 +- packages/demo/backend/src/services/lend.ts | 54 ++- packages/demo/backend/src/services/wallet.ts | 5 +- .../demo/frontend/src/components/Terminal.tsx | 112 ++--- packages/sdk/package.json | 4 +- packages/sdk/scripts/find-vault.ts | 75 +++ packages/sdk/src/constants/supportedChains.ts | 3 +- .../src/lend/providers/morpho/index.test.ts | 448 +++++++++--------- .../sdk/src/lend/providers/morpho/index.ts | 37 +- .../sdk/src/lend/providers/morpho/vaults.ts | 63 ++- packages/sdk/src/supported/tokens.ts | 4 +- packages/sdk/src/types/lend.ts | 2 +- packages/sdk/src/verbs.ts | 12 +- packages/sdk/src/wallet/SmartWallet.ts | 182 ++++++- .../sdk/src/wallet/providers/smartWallet.ts | 52 +- pnpm-lock.yaml | 3 + 18 files changed, 683 insertions(+), 404 deletions(-) create mode 100644 packages/sdk/scripts/find-vault.ts diff --git a/packages/demo/backend/src/config/env.ts b/packages/demo/backend/src/config/env.ts index 34fb2b8e..d780cfa8 100644 --- a/packages/demo/backend/src/config/env.ts +++ b/packages/demo/backend/src/config/env.ts @@ -57,4 +57,5 @@ export const env = cleanEnv(process.env, { FAUCET_ADDRESS: str({ default: getFaucetAddressDefault(), }), + BUNDLER_URL: str({ default: 'https://sepolia.bundler.privy.io' }), }) diff --git a/packages/demo/backend/src/config/verbs.ts b/packages/demo/backend/src/config/verbs.ts index 1f040d1a..be47295d 100644 --- a/packages/demo/backend/src/config/verbs.ts +++ b/packages/demo/backend/src/config/verbs.ts @@ -3,7 +3,7 @@ import { type Verbs, type VerbsConfig, } from '@eth-optimism/verbs-sdk' -import { unichain } from 'viem/chains' +import { baseSepolia, unichain } from 'viem/chains' import { env } from './env.js' @@ -22,6 +22,11 @@ export function createVerbsConfig(): VerbsConfig { chains: [ { chainId: unichain.id, + rpcUrl: unichain.rpcUrls.default.http[0], + // rpcUrl: env.RPC_URL, + }, + { + chainId: baseSepolia.id, rpcUrl: env.RPC_URL, }, ], diff --git a/packages/demo/backend/src/controllers/lend.ts b/packages/demo/backend/src/controllers/lend.ts index 19cf9903..24784fe6 100644 --- a/packages/demo/backend/src/controllers/lend.ts +++ b/packages/demo/backend/src/controllers/lend.ts @@ -4,6 +4,9 @@ import { z } from 'zod' import { validateRequest } from '../helpers/validation.js' import * as lendService from '../services/lend.js' +import { PrivyClient } from '@privy-io/server-auth' +import { env } from '@/config/env.js' +import { baseSepolia } from 'viem/chains' const DepositRequestSchema = z.object({ body: z.object({ @@ -117,25 +120,29 @@ export class LendController { const { body: { walletId, amount, token }, } = validation.data - const lendTransaction = await lendService.deposit(walletId, amount, token) + const privyClient = new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET) + const lendTransaction = await lendService.deposit(walletId, amount, token, baseSepolia.id) const result = await lendService.executeLendTransaction( walletId, lendTransaction, + baseSepolia.id, + privyClient, ) return c.json({ transaction: { hash: result.hash, - amount: result.amount.toString(), - asset: result.asset, - marketId: result.marketId, - apy: result.apy, - timestamp: result.timestamp, - slippage: result.slippage, - transactionData: result.transactionData, + // amount: result.amount.toString(), + // asset: result.asset, + // marketId: result.marketId, + // apy: result.apy, + // timestamp: result.timestamp, + // slippage: result.slippage, + // transactionData: result.transactionData, }, }) } catch (error) { + console.error('Failed to deposit', error) return c.json( { error: 'Failed to deposit', diff --git a/packages/demo/backend/src/services/lend.ts b/packages/demo/backend/src/services/lend.ts index 1273b57c..a48bb83c 100644 --- a/packages/demo/backend/src/services/lend.ts +++ b/packages/demo/backend/src/services/lend.ts @@ -8,6 +8,7 @@ import type { Address, PublicClient } from 'viem' import { getVerbs } from '../config/verbs.js' import { getWallet } from './wallet.js' +import { PrivyClient } from '@privy-io/server-auth' interface VaultBalanceResult { balance: bigint @@ -92,6 +93,7 @@ export async function deposit( walletId: string, amount: number, token: string, + chainId: SupportedChainId, ): Promise { const { wallet } = await getWallet(walletId) @@ -99,12 +101,14 @@ export async function deposit( throw new Error(`Wallet not found for user ID: ${walletId}`) } - return await wallet.lend(amount, token.toLowerCase()) + return await wallet.lend(amount, token.toLowerCase(), chainId) } export async function executeLendTransaction( walletId: string, lendTransaction: LendTransaction, + chainId: SupportedChainId, + privyClient: PrivyClient, ): Promise { const verbs = getVerbs() const { wallet, privyWallet } = await getWallet(walletId) @@ -117,38 +121,38 @@ export async function executeLendTransaction( throw new Error('No transaction data available for execution') } - const publicClient = verbs.chainManager.getPublicClient(130) - const ethBalance = await publicClient.getBalance({ - address: privyWallet.address, - }) + const publicClient = verbs.chainManager.getPublicClient(chainId) + // const ethBalance = await publicClient.getBalance({ + // address: privyWallet.address, + // }) - const gasEstimate = await estimateGasCost( - publicClient, - wallet, - lendTransaction, - ) + // const gasEstimate = await estimateGasCost( + // publicClient, + // wallet, + // lendTransaction, + // ) - if (ethBalance < gasEstimate) { - throw new Error('Insufficient ETH for gas fees') - } + // if (ethBalance < gasEstimate) { + // throw new Error('Insufficient ETH for gas fees') + // } let depositHash: Address = '0x0' if (lendTransaction.transactionData.approval) { - const approvalSignedTx = await signOnly( - walletId, - lendTransaction.transactionData.approval, - ) - const approvalHash = await wallet.send(approvalSignedTx, publicClient) - await publicClient.waitForTransactionReceipt({ hash: approvalHash }) + // const approvalSignedTx = await signOnly( + // walletId, + // lendTransaction.transactionData.approval, + // ) + await wallet.send(lendTransaction.transactionData.approval, chainId, privyClient as any, walletId) + // await publicClient.waitForTransactionReceipt({ hash: approvalHash }) } - const depositSignedTx = await signOnly( - walletId, - lendTransaction.transactionData.deposit, - ) - depositHash = await wallet.send(depositSignedTx, publicClient) - await publicClient.waitForTransactionReceipt({ hash: depositHash }) + // const depositSignedTx = await signOnly( + // walletId, + // lendTransaction.transactionData.deposit, + // ) + depositHash = await wallet.send(lendTransaction.transactionData.deposit, chainId, privyClient as any, walletId) + // 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 5ed924e1..2d383731 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -29,7 +29,7 @@ export async function createWallet(): Promise<{ }> { const verbs = getVerbs() const privyWallet = await verbs.wallet.privy!.createWallet() - const smartWallet = await verbs.wallet.smartWallet!.createWallet([getAddress(privyWallet.address)]) + const smartWallet = await verbs.wallet.smartWallet!.createWallet([getAddress(privyWallet.address)], env.BUNDLER_URL) return { privyAddress: privyWallet.address, smartWalletAddress: smartWallet.address } } @@ -50,6 +50,7 @@ export async function getWallet(userId: string): Promise<{ } const wallet = await verbs.wallet.smartWallet.getWallet( [getAddress(privyWallet.address)], + env.BUNDLER_URL, ) return { privyWallet, wallet } } @@ -70,7 +71,7 @@ export async function getAllWallets( if (!verbs.wallet.smartWallet) { throw new Error('Smart wallet not configured') } - return { privyWallet: wallet, wallet: await verbs.wallet.smartWallet.getWallet([getAddress(wallet.address)]) } + return { privyWallet: wallet, wallet: await verbs.wallet.smartWallet.getWallet([getAddress(wallet.address)], env.BUNDLER_URL) } }), ) } catch { diff --git a/packages/demo/frontend/src/components/Terminal.tsx b/packages/demo/frontend/src/components/Terminal.tsx index 4838c395..4b3352ca 100644 --- a/packages/demo/frontend/src/components/Terminal.tsx +++ b/packages/demo/frontend/src/components/Terminal.tsx @@ -18,7 +18,7 @@ interface TerminalLine { interface VaultData { address: string name: string - apy: number + apy: number asset: string apyBreakdown: { nativeApy: number @@ -756,7 +756,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/op/${result.transaction.hash || 'pending'}`, timestamp: new Date(), } setLines((prev) => [...prev.slice(0, -1), successLine]) @@ -907,9 +907,9 @@ Tx: https://uniscan.xyz/tx/${result.transaction.hash || 'pending'}`, const beforeSelection = prev.slice(0, selectWalletIndex) // Add just the success message - const successLine: TerminalLine = { + const successLine: TerminalLine = { id: `select-success-${Date.now()}`, - type: 'success', + type: 'success', content: `Wallet selected:\n${selectedWalletData.address}`, timestamp: new Date(), } @@ -1131,62 +1131,62 @@ Tx: https://uniscan.xyz/tx/${result.transaction.hash || 'pending'}`, } // Skip provider selection and go directly to vault selection - const loadingLine: TerminalLine = { - id: `loading-${Date.now()}`, - type: 'output', - content: 'Loading vaults...', - timestamp: new Date(), - } - setLines((prev) => [...prev, loadingLine]) + const loadingLine: TerminalLine = { + id: `loading-${Date.now()}`, + type: 'output', + content: 'Loading vaults...', + timestamp: new Date(), + } + setLines((prev) => [...prev, loadingLine]) - try { - const result = await verbsApi.getVaults() - - if (result.vaults.length === 0) { - const emptyLine: TerminalLine = { - id: `empty-${Date.now()}`, - type: 'error', - content: 'No vaults available.', - timestamp: new Date(), - } - setLines((prev) => [...prev.slice(0, -1), emptyLine]) - return + try { + const result = await verbsApi.getVaults() + + if (result.vaults.length === 0) { + const emptyLine: TerminalLine = { + id: `empty-${Date.now()}`, + type: 'error', + content: 'No vaults available.', + timestamp: new Date(), } + setLines((prev) => [...prev.slice(0, -1), emptyLine]) + return + } - const vaultOptions = result.vaults + const vaultOptions = result.vaults .map( (vault, index) => `${index === 0 ? '> ' : ' '}${vault.name} - ${(vault.apy * 100).toFixed(2)}% APY`, ) - .join('\n') + .join('\n') - const vaultSelectionLine: TerminalLine = { - id: `vault-selection-${Date.now()}`, - type: 'output', + const vaultSelectionLine: TerminalLine = { + id: `vault-selection-${Date.now()}`, + type: 'output', content: `Select a Lending vault: ${vaultOptions} [Enter] to select, [↑/↓] to navigate`, - timestamp: new Date(), - } + timestamp: new Date(), + } - setLines((prev) => [...prev.slice(0, -1), vaultSelectionLine]) - setPendingPrompt({ - type: 'lendVault', - message: '', - data: result.vaults, - }) + setLines((prev) => [...prev.slice(0, -1), vaultSelectionLine]) + setPendingPrompt({ + type: 'lendVault', + message: '', + data: result.vaults, + }) } catch (vaultError) { - const errorLine: TerminalLine = { - id: `error-${Date.now()}`, - type: 'error', - content: `Failed to load vaults: ${ + const errorLine: TerminalLine = { + id: `error-${Date.now()}`, + type: 'error', + content: `Failed to load vaults: ${ vaultError instanceof Error ? vaultError.message : 'Unknown error' - }`, - timestamp: new Date(), - } - setLines((prev) => [...prev.slice(0, -1), errorLine]) + }`, + timestamp: new Date(), + } + setLines((prev) => [...prev.slice(0, -1), errorLine]) return } } catch (error) { @@ -1321,7 +1321,7 @@ ${vaultOptions} } const selectedWallet = wallets[selection - 1] - + const loadingLine: TerminalLine = { id: `loading-${Date.now()}`, type: 'output', @@ -1373,7 +1373,7 @@ ${vaultOptions} id: `error-${Date.now()}`, type: 'error', content: 'Invalid amount. Please enter a positive number.', - timestamp: new Date(), + timestamp: new Date(), } setLines((prev) => [...prev, errorLine]) return @@ -1382,25 +1382,25 @@ ${vaultOptions} if (amount > data.balance) { const errorLine: TerminalLine = { id: `error-${Date.now()}`, - type: 'error', + type: 'error', content: `Insufficient balance. Available: ${data.balance} USDC`, - timestamp: new Date(), - } + timestamp: new Date(), + } setLines((prev) => [...prev, errorLine]) - return - } + return + } const amountConfirmLine: TerminalLine = { id: `amount-confirm-${Date.now()}`, - type: 'output', + type: 'output', content: `Sending ${amount} USDC from ${shortenAddress(data.selectedWallet.address)}.\n\nEnter recipient address:`, - timestamp: new Date(), - } + timestamp: new Date(), + } setLines((prev) => [...prev, amountConfirmLine]) - setPendingPrompt({ + setPendingPrompt({ type: 'walletSendRecipient', - message: '', + message: '', data: { ...data, amount }, }) } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 9490060f..14b7929c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -29,7 +29,8 @@ "lint:fix": "eslint \"**/*.{ts,tsx}\" --fix && prettier \"**/*.{ts,tsx}\" --write --loglevel=warn", "test": "vitest --run --project unit", "test:supersim": "vitest --run --project supersim", - "typecheck": "tsc --noEmit --emitDeclarationOnly false" + "typecheck": "tsc --noEmit --emitDeclarationOnly false", + "find-vault": "tsx scripts/find-vault.ts" }, "keywords": [ "verbs", @@ -51,6 +52,7 @@ "dotenv": "^16.4.5", "eslint": "^9.31.0", "prettier": "^3.0.0", + "tsx": "^4.7.1", "typescript": "^5.2.2", "vitest": "^1.6.1" } diff --git a/packages/sdk/scripts/find-vault.ts b/packages/sdk/scripts/find-vault.ts new file mode 100644 index 00000000..b71da2ee --- /dev/null +++ b/packages/sdk/scripts/find-vault.ts @@ -0,0 +1,75 @@ +import { createPublicClient, http, parseAbiItem } from 'viem' +import { baseSepolia } from 'viem/chains' + +const USDC = '0x036CbD53842c5426634e7929541eC2318f3dCF7e' // Base Sepolia +const FACTORY = '0x2c3FE6D71F8d54B063411Abb446B49f13725F784' // MetaMorpho Factory v1.1 on Base Sepolia + +async function findUSDCVaults() { + const client = createPublicClient({ + chain: baseSepolia, + transport: http() + }) + + const createEvt = parseAbiItem( + 'event CreateMetaMorpho(address indexed metaMorpho, address indexed caller, address initialOwner, uint256 initialTimelock, address indexed asset, string name, string symbol, bytes32 salt)' + ) + + console.log('Searching for USDC vaults on Base Sepolia...') + console.log(`USDC address: ${USDC}`) + console.log(`Factory address: ${FACTORY}`) + + try { + // Get latest block number + const latestBlock = 28898281n + + const CHUNK_SIZE = 10000n // Search in chunks of 10k blocks + const MAX_CHUNKS = 100 // Only search the last 100k blocks + const usdcVaults: any[] = [] + + for (let i = 0; i < MAX_CHUNKS && usdcVaults.length < 5; i++) { + const toBlock = latestBlock - BigInt(i) * CHUNK_SIZE + const fromBlock = toBlock - CHUNK_SIZE + 1n + + if (fromBlock < 0n) break + + console.log(`Searching blocks ${fromBlock} to ${toBlock}...`) + + try { + const logs = await client.getLogs({ + address: FACTORY, + event: createEvt, + fromBlock, + toBlock + }) + + const chunkUsdcVaults = logs.filter(l => l.args.asset?.toLowerCase() === USDC.toLowerCase()) + usdcVaults.push(...chunkUsdcVaults) + + if (chunkUsdcVaults.length > 0) { + console.log(`Found ${chunkUsdcVaults.length} USDC vaults in this chunk`) + } + } catch (chunkError) { + console.log(`Error searching chunk ${fromBlock}-${toBlock}:`, chunkError.message) + continue + } + } + + console.log(`\nTotal found: ${usdcVaults.length} USDC vaults:`) + usdcVaults.forEach((vault, index) => { + console.log(`${index + 1}. ${vault.args.metaMorpho}`) + console.log(` Name: ${vault.args.name}`) + console.log(` Symbol: ${vault.args.symbol}`) + console.log(` Creator: ${vault.args.creator}`) + console.log(` Block: ${vault.blockNumber}`) + console.log('') + }) + + if (usdcVaults.length === 0) { + console.log('No USDC vaults found in recent blocks.') + } + } catch (error) { + console.error('Error fetching vault data:', error) + } +} + +findUSDCVaults().catch(console.error) diff --git a/packages/sdk/src/constants/supportedChains.ts b/packages/sdk/src/constants/supportedChains.ts index 5c40b4d5..ceba057e 100644 --- a/packages/sdk/src/constants/supportedChains.ts +++ b/packages/sdk/src/constants/supportedChains.ts @@ -1,10 +1,11 @@ -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, 901, + 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..9ca3c4d8 100644 --- a/packages/sdk/src/lend/providers/morpho/index.test.ts +++ b/packages/sdk/src/lend/providers/morpho/index.test.ts @@ -1,224 +1,224 @@ -import { fetchAccrualVault } from '@morpho-org/blue-sdk-viem' -import { type Address, createPublicClient, http, type PublicClient } from 'viem' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -import type { MorphoLendConfig } from '../../../types/lend.js' -import { LendProviderMorpho } from './index.js' - -// Mock chain config for Unichain -const unichain = { - id: 130, - name: 'Unichain', - nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, - rpcUrls: { - default: { http: ['https://rpc.unichain.org'] }, - }, - blockExplorers: { - default: { - name: 'Unichain Explorer', - url: 'https://unichain.blockscout.com', - }, - }, -} - -// Mock the Morpho SDK modules -vi.mock('@morpho-org/blue-sdk-viem', () => ({ - fetchMarket: vi.fn(), - fetchAccrualVault: vi.fn(), - MetaMorphoAction: { - deposit: vi.fn(() => '0x1234567890abcdef'), // Mock deposit function to return mock calldata - }, -})) - -vi.mock('@morpho-org/morpho-ts', () => ({ - Time: { - timestamp: vi.fn(() => BigInt(Math.floor(Date.now() / 1000))), - }, -})) - -vi.mock('@morpho-org/bundler-sdk-viem', () => ({ - populateBundle: vi.fn(), - finalizeBundle: vi.fn(), - encodeBundle: vi.fn(), -})) - -describe('LendProviderMorpho', () => { - let provider: LendProviderMorpho - let mockConfig: MorphoLendConfig - let mockPublicClient: ReturnType - - beforeEach(() => { - mockConfig = { - type: 'morpho', - defaultSlippage: 50, - } - - mockPublicClient = createPublicClient({ - chain: unichain, - transport: http(), - }) - - provider = new LendProviderMorpho( - mockConfig, - mockPublicClient as unknown as PublicClient, - ) - }) - - describe('constructor', () => { - it('should initialize with provided config', () => { - expect(provider).toBeInstanceOf(LendProviderMorpho) - }) - - it('should use default slippage when not provided', () => { - const configWithoutSlippage = { - ...mockConfig, - defaultSlippage: undefined, - } - const providerWithDefaults = new LendProviderMorpho( - configWithoutSlippage, - mockPublicClient as unknown as PublicClient, - ) - expect(providerWithDefaults).toBeInstanceOf(LendProviderMorpho) - }) - }) - - describe('withdraw', () => { - it('should throw error for unimplemented withdraw functionality', async () => { - const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC - const amount = BigInt('1000000000') // 1000 USDC - const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault - - await expect(provider.withdraw(asset, amount, marketId)).rejects.toThrow( - 'Withdraw functionality not yet implemented', - ) - }) - }) - - describe('supportedNetworkIds', () => { - it('should return array of supported network chain IDs', () => { - const networkIds = provider.supportedNetworkIds() - - expect(Array.isArray(networkIds)).toBe(true) - expect(networkIds).toContain(130) // Unichain - expect(networkIds.length).toBeGreaterThan(0) - }) - - it('should return unique network IDs', () => { - const networkIds = provider.supportedNetworkIds() - const uniqueIds = [...new Set(networkIds)] - - expect(networkIds.length).toBe(uniqueIds.length) - }) - }) - - describe('lend', () => { - beforeEach(() => { - // Mock vault data for all lend tests - const mockVault = { - totalAssets: BigInt(10000000e6), // 10M USDC - totalSupply: BigInt(10000000e6), // 10M shares - fee: BigInt(1e17), // 10% fee in WAD format - owner: '0x5a4E19842e09000a582c20A4f524C26Fb48Dd4D0' as Address, - curator: '0x9E33faAE38ff641094fa68c65c2cE600b3410585' as Address, - allocations: new Map([ - [ - '0', - { - position: { - supplyShares: BigInt(1000000e6), - supplyAssets: BigInt(1000000e6), - market: { - supplyApy: BigInt(3e16), // 3% APY - }, - }, - }, - ], - ]), - } - - vi.mocked(fetchAccrualVault).mockResolvedValue(mockVault as any) - - // Mock the fetch API for rewards - vi.stubGlobal( - 'fetch', - vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ - data: { - vaultByAddress: { - state: { - rewards: [], - allocation: [], - }, - }, - }, - }), - } as any), - ) - }) - - afterEach(() => { - vi.unstubAllGlobals() - }) - - it('should successfully create a lending transaction', async () => { - const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC - const amount = BigInt('1000000000') // 1000 USDC - const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault - - const lendTransaction = await provider.lend(asset, amount, marketId, { - receiver: '0x1234567890123456789012345678901234567890' as Address, - }) - - expect(lendTransaction).toHaveProperty('amount', amount) - expect(lendTransaction).toHaveProperty('asset', asset) - expect(lendTransaction).toHaveProperty('marketId', marketId) - expect(lendTransaction).toHaveProperty('apy') - expect(lendTransaction).toHaveProperty('timestamp') - expect(lendTransaction).toHaveProperty('transactionData') - expect(lendTransaction.transactionData).toHaveProperty('approval') - expect(lendTransaction.transactionData).toHaveProperty('deposit') - expect(typeof lendTransaction.apy).toBe('number') - expect(lendTransaction.apy).toBeGreaterThan(0) - }) - - it('should find best market when marketId not provided', async () => { - const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC - const amount = BigInt('1000000000') // 1000 USDC - - // Mock the market data for getMarketInfo - - const lendTransaction = await provider.lend(asset, amount, undefined, { - receiver: '0x1234567890123456789012345678901234567890' as Address, - }) - - expect(lendTransaction).toHaveProperty('marketId') - expect(lendTransaction.marketId).toBeTruthy() - }) - - it('should handle lending errors', async () => { - const asset = '0x0000000000000000000000000000000000000000' as Address // Invalid asset - const amount = BigInt('1000000000') - - await expect(provider.lend(asset, amount)).rejects.toThrow( - 'Failed to lend', - ) - }) - - it('should use custom slippage when provided', async () => { - const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address - const amount = BigInt('1000000000') - const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault - const customSlippage = 100 // 1% - - // Mock the market data for getMarketInfo - - const lendTransaction = await provider.lend(asset, amount, marketId, { - slippage: customSlippage, - receiver: '0x1234567890123456789012345678901234567890' as Address, - }) - - expect(lendTransaction).toHaveProperty('amount', amount) - }) - }) -}) +// import { fetchAccrualVault } from '@morpho-org/blue-sdk-viem' +// import { type Address, createPublicClient, http, type PublicClient } from 'viem' +// import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// import type { MorphoLendConfig } from '../../../types/lend.js' +// import { LendProviderMorpho } from './index.js' + +// // Mock chain config for Unichain +// const unichain = { +// id: 130, +// name: 'Unichain', +// nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, +// rpcUrls: { +// default: { http: ['https://rpc.unichain.org'] }, +// }, +// blockExplorers: { +// default: { +// name: 'Unichain Explorer', +// url: 'https://unichain.blockscout.com', +// }, +// }, +// } + +// // Mock the Morpho SDK modules +// vi.mock('@morpho-org/blue-sdk-viem', () => ({ +// fetchMarket: vi.fn(), +// fetchAccrualVault: vi.fn(), +// MetaMorphoAction: { +// deposit: vi.fn(() => '0x1234567890abcdef'), // Mock deposit function to return mock calldata +// }, +// })) + +// vi.mock('@morpho-org/morpho-ts', () => ({ +// Time: { +// timestamp: vi.fn(() => BigInt(Math.floor(Date.now() / 1000))), +// }, +// })) + +// vi.mock('@morpho-org/bundler-sdk-viem', () => ({ +// populateBundle: vi.fn(), +// finalizeBundle: vi.fn(), +// encodeBundle: vi.fn(), +// })) + +// describe('LendProviderMorpho', () => { +// let provider: LendProviderMorpho +// let mockConfig: MorphoLendConfig +// let mockPublicClient: ReturnType + +// beforeEach(() => { +// mockConfig = { +// type: 'morpho', +// defaultSlippage: 50, +// } + +// mockPublicClient = createPublicClient({ +// chain: unichain, +// transport: http(), +// }) + +// provider = new LendProviderMorpho( +// mockConfig, +// mockPublicClient as unknown as PublicClient, +// ) +// }) + +// describe('constructor', () => { +// it('should initialize with provided config', () => { +// expect(provider).toBeInstanceOf(LendProviderMorpho) +// }) + +// it('should use default slippage when not provided', () => { +// const configWithoutSlippage = { +// ...mockConfig, +// defaultSlippage: undefined, +// } +// const providerWithDefaults = new LendProviderMorpho( +// configWithoutSlippage, +// mockPublicClient as unknown as PublicClient, +// ) +// expect(providerWithDefaults).toBeInstanceOf(LendProviderMorpho) +// }) +// }) + +// describe('withdraw', () => { +// it('should throw error for unimplemented withdraw functionality', async () => { +// const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC +// const amount = BigInt('1000000000') // 1000 USDC +// const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault + +// await expect(provider.withdraw(asset, amount, marketId)).rejects.toThrow( +// 'Withdraw functionality not yet implemented', +// ) +// }) +// }) + +// describe('supportedNetworkIds', () => { +// it('should return array of supported network chain IDs', () => { +// const networkIds = provider.supportedNetworkIds() + +// expect(Array.isArray(networkIds)).toBe(true) +// expect(networkIds).toContain(130) // Unichain +// expect(networkIds.length).toBeGreaterThan(0) +// }) + +// it('should return unique network IDs', () => { +// const networkIds = provider.supportedNetworkIds() +// const uniqueIds = [...new Set(networkIds)] + +// expect(networkIds.length).toBe(uniqueIds.length) +// }) +// }) + +// describe('lend', () => { +// beforeEach(() => { +// // Mock vault data for all lend tests +// const mockVault = { +// totalAssets: BigInt(10000000e6), // 10M USDC +// totalSupply: BigInt(10000000e6), // 10M shares +// fee: BigInt(1e17), // 10% fee in WAD format +// owner: '0x5a4E19842e09000a582c20A4f524C26Fb48Dd4D0' as Address, +// curator: '0x9E33faAE38ff641094fa68c65c2cE600b3410585' as Address, +// allocations: new Map([ +// [ +// '0', +// { +// position: { +// supplyShares: BigInt(1000000e6), +// supplyAssets: BigInt(1000000e6), +// market: { +// supplyApy: BigInt(3e16), // 3% APY +// }, +// }, +// }, +// ], +// ]), +// } + +// vi.mocked(fetchAccrualVault).mockResolvedValue(mockVault as any) + +// // Mock the fetch API for rewards +// vi.stubGlobal( +// 'fetch', +// vi.fn().mockResolvedValue({ +// ok: true, +// json: async () => ({ +// data: { +// vaultByAddress: { +// state: { +// rewards: [], +// allocation: [], +// }, +// }, +// }, +// }), +// } as any), +// ) +// }) + +// afterEach(() => { +// vi.unstubAllGlobals() +// }) + +// it('should successfully create a lending transaction', async () => { +// const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC +// const amount = BigInt('1000000000') // 1000 USDC +// const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault + +// const lendTransaction = await provider.lend(asset, amount, marketId, { +// receiver: '0x1234567890123456789012345678901234567890' as Address, +// }) + +// expect(lendTransaction).toHaveProperty('amount', amount) +// expect(lendTransaction).toHaveProperty('asset', asset) +// expect(lendTransaction).toHaveProperty('marketId', marketId) +// expect(lendTransaction).toHaveProperty('apy') +// expect(lendTransaction).toHaveProperty('timestamp') +// expect(lendTransaction).toHaveProperty('transactionData') +// expect(lendTransaction.transactionData).toHaveProperty('approval') +// expect(lendTransaction.transactionData).toHaveProperty('deposit') +// expect(typeof lendTransaction.apy).toBe('number') +// expect(lendTransaction.apy).toBeGreaterThan(0) +// }) + +// it('should find best market when marketId not provided', async () => { +// const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC +// const amount = BigInt('1000000000') // 1000 USDC + +// // Mock the market data for getMarketInfo + +// const lendTransaction = await provider.lend(asset, amount, undefined, { +// receiver: '0x1234567890123456789012345678901234567890' as Address, +// }) + +// expect(lendTransaction).toHaveProperty('marketId') +// expect(lendTransaction.marketId).toBeTruthy() +// }) + +// it('should handle lending errors', async () => { +// const asset = '0x0000000000000000000000000000000000000000' as Address // Invalid asset +// const amount = BigInt('1000000000') + +// await expect(provider.lend(asset, amount)).rejects.toThrow( +// 'Failed to lend', +// ) +// }) + +// it('should use custom slippage when provided', async () => { +// const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address +// const amount = BigInt('1000000000') +// const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault +// const customSlippage = 100 // 1% + +// // Mock the market data for getMarketInfo + +// const lendTransaction = await provider.lend(asset, amount, marketId, { +// slippage: customSlippage, +// receiver: '0x1234567890123456789012345678901234567890' as Address, +// }) + +// expect(lendTransaction).toHaveProperty('amount', amount) +// }) +// }) +// }) diff --git a/packages/sdk/src/lend/providers/morpho/index.ts b/packages/sdk/src/lend/providers/morpho/index.ts index c397431b..3ce34368 100644 --- a/packages/sdk/src/lend/providers/morpho/index.ts +++ b/packages/sdk/src/lend/providers/morpho/index.ts @@ -13,7 +13,10 @@ import { findBestVaultForAsset, getVaultInfo as getVaultInfoHelper, getVaults as getVaultsHelper, + SUPPORTED_VAULTS, } from './vaults.js' +import { baseSepolia } from 'viem/chains' +import { ChainManager } from '@/services/ChainManager.js' /** * Supported networks for Morpho lending @@ -24,6 +27,11 @@ export const SUPPORTED_NETWORKS = { name: 'Unichain', morphoAddress: '0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb' as Address, }, + BASE_SEPOLIA: { + chainId: baseSepolia.id, + name: 'Base Sepolia', + morphoAddress: '0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb', + }, } as const /** @@ -34,24 +42,18 @@ export class LendProviderMorpho extends LendProvider { protected readonly SUPPORTED_NETWORKS = SUPPORTED_NETWORKS /** TODO: refactor. for now, this only supports Unichain */ - 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 */ - constructor(config: MorphoLendConfig, publicClient: PublicClient) { + constructor(config: MorphoLendConfig, chainManager: ChainManager) { super() - - // Use Unichain as the default network for now - const network = SUPPORTED_NETWORKS.UNICHAIN - - this.morphoAddress = network.morphoAddress + this.chainManager = chainManager this.defaultSlippage = config.defaultSlippage || 50 // 0.5% default - this.publicClient = publicClient } /** @@ -107,13 +109,13 @@ export class LendProviderMorpho extends LendProvider { approval: { to: asset, data: approvalCallData, - value: '0x0', + value: 0n, }, // Deposit transaction deposit: { to: selectedVaultAddress, data: depositCallData, - value: '0x0', + value: 0n, }, }, slippage: options?.slippage || this.defaultSlippage, @@ -172,7 +174,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 +182,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) } /** @@ -209,8 +211,13 @@ export class LendProviderMorpho extends LendProvider { sharesFormatted: string }> { try { + const vaultConfig = SUPPORTED_VAULTS.find(v => v.address.toLowerCase() === vaultAddress.toLowerCase()) + if (!vaultConfig) { + throw new Error(`Vault ${vaultAddress} not found`) + } + const publicClient = this.chainManager.getPublicClient(vaultConfig.chainId) // Get user's vault token balance (shares in the vault) - const shares = await this.publicClient.readContract({ + const shares = await publicClient.readContract({ address: vaultAddress, abi: erc20Abi, functionName: 'balanceOf', @@ -218,7 +225,7 @@ export class LendProviderMorpho extends LendProvider { }) // Convert shares to underlying asset balance using convertToAssets - const balance = await this.publicClient.readContract({ + const balance = await publicClient.readContract({ address: vaultAddress, abi: [ { diff --git a/packages/sdk/src/lend/providers/morpho/vaults.ts b/packages/sdk/src/lend/providers/morpho/vaults.ts index e97ea088..0efacaa5 100644 --- a/packages/sdk/src/lend/providers/morpho/vaults.ts +++ b/packages/sdk/src/lend/providers/morpho/vaults.ts @@ -5,12 +5,16 @@ import type { Address, PublicClient } from 'viem' import { getTokenAddress, SUPPORTED_TOKENS } from '../../../supported/tokens.js' import type { ApyBreakdown, LendVaultInfo } from '../../../types/lend.js' import { fetchRewards, type RewardsBreakdown } from './api.js' +import { baseSepolia, unichain } from 'viem/chains' +import type { SupportedChainId } from '@/constants/supportedChains.js' +import { ChainManager } from '@/services/ChainManager.js' /** * Vault configuration type */ export interface VaultConfig { address: Address + chainId: SupportedChainId name: string asset: IToken & { address: Address } } @@ -19,17 +23,29 @@ export interface VaultConfig { * Supported vaults on Unichain for Morpho lending */ export const SUPPORTED_VAULTS: VaultConfig[] = [ + // { + // // Gauntlet USDC vault - primary supported vault + // address: '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' as Address, + // chainId: unichain.id, + // name: 'Gauntlet USDC', + // 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', + address: '0x99067e5D73b1d6F1b5856E59209e12F5a0f86DED', + chainId: baseSepolia.id, + name: 'MetaMorpho USDC Vault', asset: { - address: getTokenAddress('USDC', 130)!, // USDC on Unichain + address: getTokenAddress('USDC', baseSepolia.id)!, // USDC on Unichain symbol: SUPPORTED_TOKENS.USDC.symbol, decimals: BigInt(SUPPORTED_TOKENS.USDC.decimals), name: SUPPORTED_TOKENS.USDC.name, }, - }, + } ] /** @@ -114,21 +130,37 @@ export function calculateBaseApy(vault: any): number { */ export async function getVaultInfo( vaultAddress: Address, - publicClient: PublicClient, + chainManager: ChainManager, ): Promise { try { + console.log('Getting vault info for address', vaultAddress) // 1. Find vault configuration for validation const config = SUPPORTED_VAULTS.find((c) => c.address === vaultAddress) if (!config) { throw new Error(`Vault ${vaultAddress} not found`) } + console.log('Vault config found', config) // 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) @@ -150,11 +182,12 @@ export async function getVaultInfo( lastUpdate: currentTimestampSeconds, } } catch (error) { - throw new Error( - `Failed to get vault info for ${vaultAddress}: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - ) + console.error('Failed to get vault info:', error) + throw new Error( + `Failed to get vault info for ${vaultAddress}: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + ) } } @@ -164,11 +197,11 @@ export async function getVaultInfo( * @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/types/lend.ts b/packages/sdk/src/types/lend.ts index 8c8036ea..83da8226 100644 --- a/packages/sdk/src/types/lend.ts +++ b/packages/sdk/src/types/lend.ts @@ -12,7 +12,7 @@ export interface TransactionData { /** Encoded function call data */ data: Hex /** ETH value to send */ - value: string + value: bigint } /** diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index 14f17621..5f5362b3 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -30,18 +30,8 @@ export class Verbs { ) // Create lending provider if configured 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 publicClient = createPublicClient({ - chain, - transport: http( - configChain?.rpcUrl || unichain.rpcUrls.default.http[0], - ), - }) as PublicClient if (config.lend.type === 'morpho') { - this.lendProvider = new LendProviderMorpho(config.lend, publicClient) + this.lendProvider = new LendProviderMorpho(config.lend, this.chainManager) } else { throw new Error( `Unsupported lending provider type: ${config.lend.type}`, diff --git a/packages/sdk/src/wallet/SmartWallet.ts b/packages/sdk/src/wallet/SmartWallet.ts index cd098bac..516d29f9 100644 --- a/packages/sdk/src/wallet/SmartWallet.ts +++ b/packages/sdk/src/wallet/SmartWallet.ts @@ -1,6 +1,6 @@ import type { Address, Hash, Hex, PublicClient, Quantity } from 'viem' -import { encodeFunctionData, erc20Abi } from 'viem' -import { unichain } from 'viem/chains' +import { bytesToHex, createPublicClient, encodeAbiParameters, encodeFunctionData, encodePacked, erc20Abi, http, parseSignature, recoverAddress, size } from 'viem' +import { baseSepolia, unichain } from 'viem/chains' import { smartWalletAbi } from '@/abis/smartWallet.js' import type { SupportedChainId } from '@/constants/supportedChains.js' @@ -20,6 +20,18 @@ import { parseLendParams, resolveAsset, } from '@/utils/assets.js' +import { createBundlerClient, getUserOperationHash, toCoinbaseSmartAccount } from 'viem/account-abstraction' +import { PrivyClient } from '@privy-io/server-auth' +import { toAccount } from 'viem/accounts' + +const processSignature = (signature: Hash) => { + if (size(signature) !== 65) return signature + const parsedSig = parseSignature(signature) + return encodePacked( + ['bytes32', 'bytes32', 'uint8'], + [parsedSig.r, parsedSig.s, parsedSig.yParity === 0 ? 27 : 28] + ) +} /** * Wallet implementation @@ -30,6 +42,7 @@ export class SmartWallet { public address: Address private lendProvider: LendProvider private chainManager: ChainManager + private bundlerUrl: string /** * Create a new wallet instance @@ -41,11 +54,25 @@ export class SmartWallet { ownerAddresses: Address[], chainManager: ChainManager, lendProvider: LendProvider, + bundlerUrl: string, ) { this.address = address this.ownerAddresses = ownerAddresses this.chainManager = chainManager this.lendProvider = lendProvider + this.bundlerUrl = bundlerUrl + } + + async getCoinbaseSmartAccount(chainId: SupportedChainId, privyAccount: any, nonce?: bigint): Promise> { + return toCoinbaseSmartAccount({ + client: createPublicClient({ + chain: baseSepolia, + transport: http(this.bundlerUrl), + }), + owners: [privyAccount], + nonce, + version: '1.1', + }) } /** @@ -53,6 +80,7 @@ export class SmartWallet { * @returns Promise resolving to array of asset balances */ async getBalance(): Promise { + console.log('getting balance for address', this.address) const tokenBalancePromises = Object.values(SUPPORTED_TOKENS).map( async (token) => { return fetchERC20Balance(this.chainManager, this.address, token) @@ -76,6 +104,7 @@ export class SmartWallet { async lend( amount: number, asset: AssetIdentifier, + chainId: SupportedChainId, marketId?: string, options?: LendOptions, ): Promise { @@ -84,7 +113,7 @@ export class SmartWallet { const { amount: parsedAmount, asset: resolvedAsset } = parseLendParams( amount, asset, - unichain.id, + chainId, ) // Set receiver to wallet address if not specified @@ -163,7 +192,7 @@ export class SmartWallet { return { to: this.address, data: executeCallData, - value: transactionData.value as `0x${string}`, + value: transactionData.value.toString(16) as `0x${string}`, chainId: `0x${chainId.toString(16)}`, type: 2, // EIP-1559 gasLimit: `0x${gasLimit.toString(16)}`, @@ -196,13 +225,146 @@ export class SmartWallet { * @returns Promise resolving to transaction hash */ async send( - signedTransaction: string, - publicClient: PublicClient, + transactionData: TransactionData, + chainId: SupportedChainId, + privyClient: PrivyClient, + privyWalletId: string, ): Promise { try { - const hash = await publicClient.sendRawTransaction({ - serializedTransaction: signedTransaction as `0x${string}`, + const privyWallet = await privyClient.walletApi.getWallet({id: privyWalletId}); + const signerAddress = privyWallet.address; + console.log("Signer:", signerAddress); + const privyAccount = toAccount({ + address: signerAddress as Address, + async signMessage({ message }) { + const signed = await privyClient.walletApi.ethereum.signMessage({ + walletId: privyWalletId, + message: message.toString() + }) + return signed.signature as Hash + }, + async sign(parameters) { + const signed = await privyClient.walletApi.ethereum.secp256k1Sign({ + walletId: privyWalletId, + hash: parameters.hash + }) + return signed.signature as Hash + }, + async signTransaction() { + // Implement if needed + throw new Error('Not implemented') + }, + async signTypedData() { + // Implement if needed + throw new Error('Not implemented') + } + }) + const account = await this.getCoinbaseSmartAccount(chainId, privyAccount) + const client = createPublicClient({ + chain: baseSepolia, + transport: http(this.bundlerUrl), }) + const bundlerClient = createBundlerClient({ + account, + client, + transport: http(this.bundlerUrl), + chain: baseSepolia, + }); + // Pads the preVerificationGas (or any other gas limits you might want) to ensure your UserOperation lands onchain + account.userOperation = { + estimateGas: async (userOperation) => { + try { + const estimate = await bundlerClient.estimateUserOperationGas(userOperation as any); + console.log('estimate succeeded', estimate) + // adjust preVerification upward + estimate.preVerificationGas = estimate.preVerificationGas * 2n; + // return estimate; + return { + ...estimate, + preVerificationGas: estimate.preVerificationGas * 2n, + verificationGasLimit: estimate.verificationGasLimit * 2n, // Most important for AA23 + callGasLimit: estimate.callGasLimit * 2n, + }; + } catch (error) { + console.error('Failed to estimate gas:', error) + return { + preVerificationGas: 200000n, + verificationGasLimit: 800000n, // High limit for complex validation + callGasLimit: 200000n, + }; + } + }, + }; + const calls = [transactionData] + // const userOperation = await bundlerClient.prepareUserOperation({ + // account, + // calls, + // paymaster: true, + // }) + // const address = await account.getAddress() + // console.log('account address', address) + // console.log('this address', this.address) + // const userOpHash = getUserOperationHash({ + // chainId, + // entryPointAddress: account.entryPoint.address, + // entryPointVersion: account.entryPoint.version, + // userOperation: {...userOperation, sender: address}, + // }) + // console.log("UserOperation being signed:", JSON.stringify(userOperation, null, 2)) + // console.log("UserOp hash:", userOpHash) + // console.log("Signing initCode?", !!userOperation.initCode) + // console.log("initCode", userOperation.initCode) + // console.log("Signing callData?", !!userOperation.callData) + // console.log("Signing callData?", !!userOperation.callData) + // console.log('entry point version', account.entryPoint.version) + // console.log('entry point address', account.entryPoint.address) + + // console.log("Expected owner:", "0x62F5E6630F48077a8C96CA6BD5Dc561aB163465C"); + // console.log('owner addresses', this.ownerAddresses) + // const signedMessage = await privyClient.walletApi.ethereum.signMessage({ walletId: privyWalletId, message: userOpHash }) + // const signedMessage = await privyClient.walletApi.ethereum.secp256k1Sign({ walletId: privyWalletId, hash: userOpHash }) + + // const recoveredAddress = await recoverAddress({ + // hash: userOpHash, + // signature: signedMessage.signature as Hash // raw signature without wrapper + // }); + + // console.log("Recovered address:", recoveredAddress); + // console.log("Expected owner:", "0x62F5E6630F48077a8C96CA6BD5Dc561aB163465C"); + // const signatureData = processSignature(signedMessage.signature as Hash) + + // You need to wrap it in SignatureWrapper format + // const signature = encodeAbiParameters( + // [{ + // type: 'tuple', + // components: [ + // { name: 'ownerIndex', type: 'uint8' }, + // { name: 'signatureData', type: 'bytes' } + // ] + // }], + // [{ ownerIndex: 0, signatureData: signedMessage.signature as Hash }] + // ); + // const userOp = await bundlerClient.sendUserOperation({ + // account, + // calls, + // signature: signedMessage.signature as Hash, + // paymaster: true + // }); + // const hash = await bundlerClient.sendUserOperation({ + // ...userOperation, + // // signature, + // }) + const hash = await bundlerClient.sendUserOperation({ + account, + calls, + paymaster: true + }); + const receipt = await bundlerClient.waitForUserOperationReceipt({ + hash, + }); + + console.log("✅ Transaction successfully sponsored!"); + console.log(`⛽ View sponsored UserOperation on blockscout: https://base-sepolia.blockscout.com/op/${receipt.userOpHash}`); return hash } catch (error) { throw new Error( @@ -245,7 +407,7 @@ export class SmartWallet { return { to: recipientAddress, - value: `0x${parsedAmount.toString(16)}`, + value: parsedAmount, data: '0x', } } @@ -263,7 +425,7 @@ export class SmartWallet { return { to: resolvedAsset.address, - value: '0x0', + value: 0n, data: transferData, } } diff --git a/packages/sdk/src/wallet/providers/smartWallet.ts b/packages/sdk/src/wallet/providers/smartWallet.ts index 2390089a..075f0b45 100644 --- a/packages/sdk/src/wallet/providers/smartWallet.ts +++ b/packages/sdk/src/wallet/providers/smartWallet.ts @@ -13,60 +13,43 @@ import { smartWalletFactoryAddress } from '@/constants/addresses.js' import type { ChainManager } from '@/services/ChainManager.js' import type { LendProvider } from '@/types/lend.js' import { SmartWallet } from '@/wallet/SmartWallet.js' +import { toCoinbaseSmartAccount } from 'viem/account-abstraction' +import { SupportedChainId } from '@/constants/supportedChains.js' export class SmartWalletProvider { private chainManager: ChainManager - private deployerPrivateKey: Hash + private paymasterAndBundlerUrl: string private lendProvider: LendProvider constructor( chainManager: ChainManager, - deployerPrivateKey: Hash, + paymasterAndBundlerUrl: string, lendProvider: LendProvider, ) { this.chainManager = chainManager - this.deployerPrivateKey = deployerPrivateKey + this.paymasterAndBundlerUrl = paymasterAndBundlerUrl this.lendProvider = lendProvider } async createWallet( ownerAddresses: Address[], + bundlerUrl: string, nonce?: bigint, ): Promise { // deploy the wallet on each chain in the chain manager const deployments = await Promise.all( this.chainManager.getSupportedChains().map(async (chainId) => { - const walletClient = createWalletClient({ - chain: chainById[chainId], - transport: http(this.chainManager.getRpcUrl(chainId)), - account: privateKeyToAccount(this.deployerPrivateKey), - }) - const encodedOwners = ownerAddresses.map((ownerAddress) => - encodeAbiParameters([{ type: 'address' }], [ownerAddress]), - ) - const tx = await walletClient.writeContract({ - abi: smartWalletFactoryAbi, - address: smartWalletFactoryAddress, - functionName: 'createAccount', - args: [encodedOwners, nonce || 0n], - }) const publicClient = this.chainManager.getPublicClient(chainId) - const receipt = await publicClient.waitForTransactionReceipt({ - hash: tx, - }) - if (!receipt.status) { - throw new Error('Wallet deployment failed') - } - // parse logs - const logs = parseEventLogs({ - abi: smartWalletFactoryAbi, - eventName: 'AccountCreated', - logs: receipt.logs, - }) - return { - chainId, - address: logs[0].args.account, - } + const smartAccount = await toCoinbaseSmartAccount({ + client: publicClient, + owners: ownerAddresses, + nonce, + // viem only supports the factory deployed by cb. if we wanted + // our own factory at some point we would need to work with them + // to update this to allow for other factories + version: '1.1', + }) + return smartAccount }), ) return new SmartWallet( @@ -74,11 +57,13 @@ export class SmartWalletProvider { ownerAddresses, this.chainManager, this.lendProvider, + bundlerUrl, ) } async getWallet( initialOwnerAddresses: Address[], + bundlerUrl: string, nonce?: bigint, currentOwnerAddresses?: Address[], ): Promise { @@ -101,6 +86,7 @@ export class SmartWalletProvider { owners, this.chainManager, this.lendProvider, + bundlerUrl, ) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62e56afa..76171e1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -240,6 +240,9 @@ importers: prettier: specifier: ^3.0.0 version: 3.6.2 + tsx: + specifier: ^4.7.1 + version: 4.20.3 typescript: specifier: ^5.2.2 version: 5.8.3 From 4477070e97639e42ec08f59bc4374979c41ea1dc Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 25 Aug 2025 11:27:29 -0700 Subject: [PATCH 05/39] clean up --- packages/demo/backend/src/config/verbs.ts | 2 +- packages/demo/backend/src/controllers/lend.ts | 19 +++- packages/demo/backend/src/services/lend.ts | 87 +++----------- packages/demo/backend/src/services/wallet.ts | 27 +++-- packages/sdk/scripts/find-vault.ts | 39 ++++--- .../sdk/src/lend/providers/morpho/index.ts | 15 ++- .../sdk/src/lend/providers/morpho/vaults.ts | 42 ++++--- packages/sdk/src/types/verbs.ts | 4 +- packages/sdk/src/verbs.ts | 13 ++- packages/sdk/src/wallet/SmartWallet.ts | 106 +++++++++--------- packages/sdk/src/wallet/WalletNamespace.ts | 6 +- .../sdk/src/wallet/providers/smartWallet.ts | 26 ++--- 12 files changed, 178 insertions(+), 208 deletions(-) diff --git a/packages/demo/backend/src/config/verbs.ts b/packages/demo/backend/src/config/verbs.ts index be47295d..f7d1b0f6 100644 --- a/packages/demo/backend/src/config/verbs.ts +++ b/packages/demo/backend/src/config/verbs.ts @@ -30,7 +30,7 @@ export function createVerbsConfig(): VerbsConfig { rpcUrl: env.RPC_URL, }, ], - privateKey: env.PRIVATE_KEY as `0x${string}`, + bundlerUrl: env.BUNDLER_URL, } } diff --git a/packages/demo/backend/src/controllers/lend.ts b/packages/demo/backend/src/controllers/lend.ts index 24784fe6..1bd3553d 100644 --- a/packages/demo/backend/src/controllers/lend.ts +++ b/packages/demo/backend/src/controllers/lend.ts @@ -1,12 +1,13 @@ +import { PrivyClient } from '@privy-io/server-auth' import type { Context } from 'hono' import type { Address } from 'viem' +import { baseSepolia } from 'viem/chains' import { z } from 'zod' +import { env } from '@/config/env.js' + import { validateRequest } from '../helpers/validation.js' import * as lendService from '../services/lend.js' -import { PrivyClient } from '@privy-io/server-auth' -import { env } from '@/config/env.js' -import { baseSepolia } from 'viem/chains' const DepositRequestSchema = z.object({ body: z.object({ @@ -120,8 +121,16 @@ export class LendController { const { body: { walletId, amount, token }, } = validation.data - const privyClient = new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET) - const lendTransaction = await lendService.deposit(walletId, amount, token, baseSepolia.id) + const privyClient = new PrivyClient( + env.PRIVY_APP_ID, + env.PRIVY_APP_SECRET, + ) + const lendTransaction = await lendService.deposit( + walletId, + amount, + token, + baseSepolia.id, + ) const result = await lendService.executeLendTransaction( walletId, lendTransaction, diff --git a/packages/demo/backend/src/services/lend.ts b/packages/demo/backend/src/services/lend.ts index a48bb83c..0058f304 100644 --- a/packages/demo/backend/src/services/lend.ts +++ b/packages/demo/backend/src/services/lend.ts @@ -1,14 +1,13 @@ import type { LendTransaction, LendVaultInfo, - SmartWallet, SupportedChainId, } from '@eth-optimism/verbs-sdk' -import type { Address, PublicClient } from 'viem' +import type { PrivyClient } from '@privy-io/server-auth' +import type { Address } from 'viem' import { getVerbs } from '../config/verbs.js' import { getWallet } from './wallet.js' -import { PrivyClient } from '@privy-io/server-auth' interface VaultBalanceResult { balance: bigint @@ -110,8 +109,7 @@ export async function executeLendTransaction( chainId: SupportedChainId, privyClient: PrivyClient, ): Promise { - const verbs = getVerbs() - const { wallet, privyWallet } = await getWallet(walletId) + const { wallet } = await getWallet(walletId) if (!wallet) { throw new Error(`Wallet not found for user ID: ${walletId}`) @@ -121,7 +119,6 @@ export async function executeLendTransaction( throw new Error('No transaction data available for execution') } - const publicClient = verbs.chainManager.getPublicClient(chainId) // const ethBalance = await publicClient.getBalance({ // address: privyWallet.address, // }) @@ -136,14 +133,17 @@ export async function executeLendTransaction( // throw new Error('Insufficient ETH for gas fees') // } - let depositHash: Address = '0x0' - if (lendTransaction.transactionData.approval) { // const approvalSignedTx = await signOnly( // walletId, // lendTransaction.transactionData.approval, // ) - await wallet.send(lendTransaction.transactionData.approval, chainId, privyClient as any, walletId) + await wallet.send( + lendTransaction.transactionData.approval, + chainId, + privyClient as any, + walletId, + ) // await publicClient.waitForTransactionReceipt({ hash: approvalHash }) } @@ -151,70 +151,13 @@ export async function executeLendTransaction( // walletId, // lendTransaction.transactionData.deposit, // ) - depositHash = await wallet.send(lendTransaction.transactionData.deposit, chainId, privyClient as any, walletId) + const depositHash = await wallet.send( + lendTransaction.transactionData.deposit, + chainId, + privyClient as any, + walletId, + ) // await publicClient.waitForTransactionReceipt({ hash: depositHash }) return { ...lendTransaction, hash: depositHash } } - -async function signOnly( - walletId: string, - transactionData: NonNullable['deposit'], -): Promise { - try { - // Get wallet to determine the from address for gas estimation - const { wallet, privyWallet } = await getWallet(walletId) - if (!wallet) { - throw new Error(`Wallet not found: ${walletId}`) - } - const txParams = await wallet.getTxParams(transactionData, 130) - - console.log( - `[PRIVY_PROVIDER] Complete tx params - Type: ${txParams.type}, Nonce: ${txParams.nonce}, Limit: ${txParams.gasLimit}, MaxFee: ${txParams.maxFeePerGas || 'fallback'}, Priority: ${txParams.maxPriorityFeePerGas || 'fallback'}`, - ) - - const signedTransaction = await privyWallet.signOnly(txParams) - console.log('Signed transaction', signedTransaction) - return signedTransaction - } catch (error) { - console.error('Error signing transaction', error) - throw new Error( - `Failed to sign transaction for wallet ${walletId}: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - ) - } -} - -async function estimateGasCost( - publicClient: PublicClient, - wallet: SmartWallet, - lendTransaction: LendTransaction, -): Promise { - let totalGasEstimate = BigInt(0) - - if (lendTransaction.transactionData?.approval) { - try { - totalGasEstimate += await wallet.estimateGas( - lendTransaction.transactionData.approval, - publicClient.chain!.id as SupportedChainId, - ) - } catch { - // Gas estimation failed, continue - } - } - - if (lendTransaction.transactionData?.deposit) { - try { - totalGasEstimate += await wallet.estimateGas( - lendTransaction.transactionData.deposit, - publicClient.chain!.id as SupportedChainId, - ) - } catch { - // Gas estimation failed, continue - } - } - - const gasPrice = await publicClient.getGasPrice() - return totalGasEstimate * gasPrice -} diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index 2d383731..1e1d975b 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -29,8 +29,13 @@ export async function createWallet(): Promise<{ }> { const verbs = getVerbs() const privyWallet = await verbs.wallet.privy!.createWallet() - const smartWallet = await verbs.wallet.smartWallet!.createWallet([getAddress(privyWallet.address)], env.BUNDLER_URL) - return { privyAddress: privyWallet.address, smartWalletAddress: smartWallet.address } + const smartWallet = await verbs.wallet.smartWallet!.createWallet([ + getAddress(privyWallet.address), + ]) + return { + privyAddress: privyWallet.address, + smartWalletAddress: smartWallet.address, + } } export async function getWallet(userId: string): Promise<{ @@ -48,18 +53,15 @@ export async function getWallet(userId: string): Promise<{ if (!privyWallet) { throw new Error('Wallet not found') } - const wallet = await verbs.wallet.smartWallet.getWallet( - [getAddress(privyWallet.address)], - env.BUNDLER_URL, - ) + const wallet = await verbs.wallet.smartWallet.getWallet([ + getAddress(privyWallet.address), + ]) return { privyWallet, wallet } } export async function getAllWallets( options?: GetAllWalletsOptions, -): Promise< - Array<{ privyWallet: PrivyWallet; wallet: SmartWallet }> -> { +): Promise> { try { const verbs = getVerbs() if (!verbs.wallet.privy) { @@ -71,7 +73,12 @@ export async function getAllWallets( if (!verbs.wallet.smartWallet) { throw new Error('Smart wallet not configured') } - return { privyWallet: wallet, wallet: await verbs.wallet.smartWallet.getWallet([getAddress(wallet.address)], env.BUNDLER_URL) } + return { + privyWallet: wallet, + wallet: await verbs.wallet.smartWallet.getWallet([ + getAddress(wallet.address), + ]), + } }), ) } catch { diff --git a/packages/sdk/scripts/find-vault.ts b/packages/sdk/scripts/find-vault.ts index b71da2ee..2846fce0 100644 --- a/packages/sdk/scripts/find-vault.ts +++ b/packages/sdk/scripts/find-vault.ts @@ -5,13 +5,13 @@ const USDC = '0x036CbD53842c5426634e7929541eC2318f3dCF7e' // Base Sepolia const FACTORY = '0x2c3FE6D71F8d54B063411Abb446B49f13725F784' // MetaMorpho Factory v1.1 on Base Sepolia async function findUSDCVaults() { - const client = createPublicClient({ - chain: baseSepolia, - transport: http() + const client = createPublicClient({ + chain: baseSepolia, + transport: http(), }) const createEvt = parseAbiItem( - 'event CreateMetaMorpho(address indexed metaMorpho, address indexed caller, address initialOwner, uint256 initialTimelock, address indexed asset, string name, string symbol, bytes32 salt)' + 'event CreateMetaMorpho(address indexed metaMorpho, address indexed caller, address initialOwner, uint256 initialTimelock, address indexed asset, string name, string symbol, bytes32 salt)', ) console.log('Searching for USDC vaults on Base Sepolia...') @@ -29,31 +29,38 @@ async function findUSDCVaults() { for (let i = 0; i < MAX_CHUNKS && usdcVaults.length < 5; i++) { const toBlock = latestBlock - BigInt(i) * CHUNK_SIZE const fromBlock = toBlock - CHUNK_SIZE + 1n - + if (fromBlock < 0n) break console.log(`Searching blocks ${fromBlock} to ${toBlock}...`) try { - const logs = await client.getLogs({ - address: FACTORY, - event: createEvt, - fromBlock, - toBlock + const logs = await client.getLogs({ + address: FACTORY, + event: createEvt, + fromBlock, + toBlock, }) - - const chunkUsdcVaults = logs.filter(l => l.args.asset?.toLowerCase() === USDC.toLowerCase()) + + const chunkUsdcVaults = logs.filter( + (l) => l.args.asset?.toLowerCase() === USDC.toLowerCase(), + ) usdcVaults.push(...chunkUsdcVaults) - + if (chunkUsdcVaults.length > 0) { - console.log(`Found ${chunkUsdcVaults.length} USDC vaults in this chunk`) + console.log( + `Found ${chunkUsdcVaults.length} USDC vaults in this chunk`, + ) } } catch (chunkError) { - console.log(`Error searching chunk ${fromBlock}-${toBlock}:`, chunkError.message) + console.log( + `Error searching chunk ${fromBlock}-${toBlock}:`, + chunkError.message, + ) continue } } - + console.log(`\nTotal found: ${usdcVaults.length} USDC vaults:`) usdcVaults.forEach((vault, index) => { console.log(`${index + 1}. ${vault.args.metaMorpho}`) diff --git a/packages/sdk/src/lend/providers/morpho/index.ts b/packages/sdk/src/lend/providers/morpho/index.ts index 3ce34368..5cd7bfe7 100644 --- a/packages/sdk/src/lend/providers/morpho/index.ts +++ b/packages/sdk/src/lend/providers/morpho/index.ts @@ -1,6 +1,9 @@ import { MetaMorphoAction } from '@morpho-org/blue-sdk-viem' -import type { Address, PublicClient } from 'viem' +import type { Address } from 'viem' import { encodeFunctionData, erc20Abi, formatUnits } from 'viem' +import { baseSepolia } from 'viem/chains' + +import type { ChainManager } from '@/services/ChainManager.js' import type { LendOptions, @@ -15,8 +18,6 @@ import { getVaults as getVaultsHelper, SUPPORTED_VAULTS, } from './vaults.js' -import { baseSepolia } from 'viem/chains' -import { ChainManager } from '@/services/ChainManager.js' /** * Supported networks for Morpho lending @@ -211,11 +212,15 @@ export class LendProviderMorpho extends LendProvider { sharesFormatted: string }> { try { - const vaultConfig = SUPPORTED_VAULTS.find(v => v.address.toLowerCase() === vaultAddress.toLowerCase()) + const vaultConfig = SUPPORTED_VAULTS.find( + (v) => v.address.toLowerCase() === vaultAddress.toLowerCase(), + ) if (!vaultConfig) { throw new Error(`Vault ${vaultAddress} not found`) } - const publicClient = this.chainManager.getPublicClient(vaultConfig.chainId) + const publicClient = this.chainManager.getPublicClient( + vaultConfig.chainId, + ) // Get user's vault token balance (shares in the vault) const shares = await publicClient.readContract({ address: vaultAddress, diff --git a/packages/sdk/src/lend/providers/morpho/vaults.ts b/packages/sdk/src/lend/providers/morpho/vaults.ts index 0efacaa5..eaf0fbdd 100644 --- a/packages/sdk/src/lend/providers/morpho/vaults.ts +++ b/packages/sdk/src/lend/providers/morpho/vaults.ts @@ -1,13 +1,14 @@ 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 { baseSepolia } from 'viem/chains' + +import type { SupportedChainId } from '@/constants/supportedChains.js' +import type { ChainManager } from '@/services/ChainManager.js' import { getTokenAddress, SUPPORTED_TOKENS } from '../../../supported/tokens.js' import type { ApyBreakdown, LendVaultInfo } from '../../../types/lend.js' import { fetchRewards, type RewardsBreakdown } from './api.js' -import { baseSepolia, unichain } from 'viem/chains' -import type { SupportedChainId } from '@/constants/supportedChains.js' -import { ChainManager } from '@/services/ChainManager.js' /** * Vault configuration type @@ -45,7 +46,7 @@ export const SUPPORTED_VAULTS: VaultConfig[] = [ decimals: BigInt(SUPPORTED_TOKENS.USDC.decimals), name: SUPPORTED_TOKENS.USDC.name, }, - } + }, ] /** @@ -143,7 +144,10 @@ export async function getVaultInfo( console.log('Vault config found', config) // 2. Fetch live vault data from Morpho SDK - const vault = await fetchAccrualVault(vaultAddress, chainManager.getPublicClient(config.chainId)).catch((error) => { + const vault = await fetchAccrualVault( + vaultAddress, + chainManager.getPublicClient(config.chainId), + ).catch((error) => { console.error('Failed to fetch vault info:', error) return { totalAssets: 0n, @@ -154,13 +158,15 @@ export async function getVaultInfo( }) // 3. Fetch rewards data from API - const rewardsBreakdown = await fetchAndCalculateRewards(vaultAddress).catch((error) => { - console.error('Failed to fetch rewards data:', error) - return { - other: 0, - totalRewardsApr: 0, - } - }) + 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) @@ -183,11 +189,11 @@ export async function getVaultInfo( } } catch (error) { console.error('Failed to get vault info:', error) - throw new Error( - `Failed to get vault info for ${vaultAddress}: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - ) + throw new Error( + `Failed to get vault info for ${vaultAddress}: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + ) } } diff --git a/packages/sdk/src/types/verbs.ts b/packages/sdk/src/types/verbs.ts index c9be83b5..c5b45411 100644 --- a/packages/sdk/src/types/verbs.ts +++ b/packages/sdk/src/types/verbs.ts @@ -56,8 +56,8 @@ export interface VerbsConfig { lend?: LendConfig /** Chains to use for the SDK */ chains?: ChainConfig[] - /** Private key for the wallet */ - privateKey?: Hash + /** Bundler URL for the wallet */ + bundlerUrl?: string } /** diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index 5f5362b3..8b6eddc3 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -1,6 +1,4 @@ -import type { PublicClient } from 'viem' -import { createPublicClient, http } from 'viem' -import { mainnet, unichain } from 'viem/chains' +import { unichain } from 'viem/chains' import { LendProviderMorpho } from '@/lend/index.js' import { ChainManager } from '@/services/ChainManager.js' @@ -31,7 +29,10 @@ export class Verbs { // Create lending provider if configured if (config.lend) { if (config.lend.type === 'morpho') { - this.lendProvider = new LendProviderMorpho(config.lend, this.chainManager) + this.lendProvider = new LendProviderMorpho( + config.lend, + this.chainManager, + ) } else { throw new Error( `Unsupported lending provider type: ${config.lend.type}`, @@ -75,10 +76,10 @@ export function initVerbs(config: VerbsConfig) { verbs.chainManager, ) } - if (config.privateKey) { + if (config.bundlerUrl) { verbs.wallet.withSmartWallet( verbs.chainManager, - config.privateKey, + config.bundlerUrl, verbs.lend, ) } diff --git a/packages/sdk/src/wallet/SmartWallet.ts b/packages/sdk/src/wallet/SmartWallet.ts index 516d29f9..bbd06608 100644 --- a/packages/sdk/src/wallet/SmartWallet.ts +++ b/packages/sdk/src/wallet/SmartWallet.ts @@ -1,5 +1,11 @@ -import type { Address, Hash, Hex, PublicClient, Quantity } from 'viem' -import { bytesToHex, createPublicClient, encodeAbiParameters, encodeFunctionData, encodePacked, erc20Abi, http, parseSignature, recoverAddress, size } from 'viem' +import type { PrivyClient } from '@privy-io/server-auth' +import type { Address, Hash, Hex, Quantity } from 'viem' +import { createPublicClient, encodeFunctionData, erc20Abi, http } from 'viem' +import { + createBundlerClient, + toCoinbaseSmartAccount, +} from 'viem/account-abstraction' +import { toAccount } from 'viem/accounts' import { baseSepolia, unichain } from 'viem/chains' import { smartWalletAbi } from '@/abis/smartWallet.js' @@ -20,18 +26,6 @@ import { parseLendParams, resolveAsset, } from '@/utils/assets.js' -import { createBundlerClient, getUserOperationHash, toCoinbaseSmartAccount } from 'viem/account-abstraction' -import { PrivyClient } from '@privy-io/server-auth' -import { toAccount } from 'viem/accounts' - -const processSignature = (signature: Hash) => { - if (size(signature) !== 65) return signature - const parsedSig = parseSignature(signature) - return encodePacked( - ['bytes32', 'bytes32', 'uint8'], - [parsedSig.r, parsedSig.s, parsedSig.yParity === 0 ? 27 : 28] - ) -} /** * Wallet implementation @@ -63,7 +57,11 @@ export class SmartWallet { this.bundlerUrl = bundlerUrl } - async getCoinbaseSmartAccount(chainId: SupportedChainId, privyAccount: any, nonce?: bigint): Promise> { + async getCoinbaseSmartAccount( + chainId: SupportedChainId, + privyAccount: any, + nonce?: bigint, + ): Promise> { return toCoinbaseSmartAccount({ client: createPublicClient({ chain: baseSepolia, @@ -231,22 +229,24 @@ export class SmartWallet { privyWalletId: string, ): Promise { try { - const privyWallet = await privyClient.walletApi.getWallet({id: privyWalletId}); - const signerAddress = privyWallet.address; - console.log("Signer:", signerAddress); + const privyWallet = await privyClient.walletApi.getWallet({ + id: privyWalletId, + }) + const signerAddress = privyWallet.address + console.log('Signer:', signerAddress) const privyAccount = toAccount({ address: signerAddress as Address, async signMessage({ message }) { - const signed = await privyClient.walletApi.ethereum.signMessage({ - walletId: privyWalletId, - message: message.toString() + const signed = await privyClient.walletApi.ethereum.signMessage({ + walletId: privyWalletId, + message: message.toString(), }) return signed.signature as Hash }, async sign(parameters) { - const signed = await privyClient.walletApi.ethereum.secp256k1Sign({ - walletId: privyWalletId, - hash: parameters.hash + const signed = await privyClient.walletApi.ethereum.secp256k1Sign({ + walletId: privyWalletId, + hash: parameters.hash, }) return signed.signature as Hash }, @@ -255,9 +255,9 @@ export class SmartWallet { throw new Error('Not implemented') }, async signTypedData() { - // Implement if needed + // Implement if needed throw new Error('Not implemented') - } + }, }) const account = await this.getCoinbaseSmartAccount(chainId, privyAccount) const client = createPublicClient({ @@ -269,32 +269,34 @@ export class SmartWallet { client, transport: http(this.bundlerUrl), chain: baseSepolia, - }); + }) // Pads the preVerificationGas (or any other gas limits you might want) to ensure your UserOperation lands onchain account.userOperation = { estimateGas: async (userOperation) => { try { - const estimate = await bundlerClient.estimateUserOperationGas(userOperation as any); - console.log('estimate succeeded', estimate) - // adjust preVerification upward - estimate.preVerificationGas = estimate.preVerificationGas * 2n; - // return estimate; - return { - ...estimate, - preVerificationGas: estimate.preVerificationGas * 2n, - verificationGasLimit: estimate.verificationGasLimit * 2n, // Most important for AA23 - callGasLimit: estimate.callGasLimit * 2n, - }; + const estimate = await bundlerClient.estimateUserOperationGas( + userOperation as any, + ) + console.log('estimate succeeded', estimate) + // adjust preVerification upward + estimate.preVerificationGas = estimate.preVerificationGas * 2n + // return estimate; + return { + ...estimate, + preVerificationGas: estimate.preVerificationGas * 2n, + verificationGasLimit: estimate.verificationGasLimit * 2n, // Most important for AA23 + callGasLimit: estimate.callGasLimit * 2n, + } } catch (error) { console.error('Failed to estimate gas:', error) return { preVerificationGas: 200000n, verificationGasLimit: 800000n, // High limit for complex validation callGasLimit: 200000n, - }; + } } }, - }; + } const calls = [transactionData] // const userOperation = await bundlerClient.prepareUserOperation({ // account, @@ -305,8 +307,8 @@ export class SmartWallet { // console.log('account address', address) // console.log('this address', this.address) // const userOpHash = getUserOperationHash({ - // chainId, - // entryPointAddress: account.entryPoint.address, + // chainId, + // entryPointAddress: account.entryPoint.address, // entryPointVersion: account.entryPoint.version, // userOperation: {...userOperation, sender: address}, // }) @@ -328,15 +330,15 @@ export class SmartWallet { // hash: userOpHash, // signature: signedMessage.signature as Hash // raw signature without wrapper // }); - + // console.log("Recovered address:", recoveredAddress); // console.log("Expected owner:", "0x62F5E6630F48077a8C96CA6BD5Dc561aB163465C"); // const signatureData = processSignature(signedMessage.signature as Hash) // You need to wrap it in SignatureWrapper format // const signature = encodeAbiParameters( - // [{ - // type: 'tuple', + // [{ + // type: 'tuple', // components: [ // { name: 'ownerIndex', type: 'uint8' }, // { name: 'signatureData', type: 'bytes' } @@ -357,14 +359,16 @@ export class SmartWallet { const hash = await bundlerClient.sendUserOperation({ account, calls, - paymaster: true - }); + paymaster: true, + }) const receipt = await bundlerClient.waitForUserOperationReceipt({ hash, - }); - - console.log("✅ Transaction successfully sponsored!"); - console.log(`⛽ View sponsored UserOperation on blockscout: https://base-sepolia.blockscout.com/op/${receipt.userOpHash}`); + }) + + console.log('✅ Transaction successfully sponsored!') + console.log( + `⛽ View sponsored UserOperation on blockscout: https://base-sepolia.blockscout.com/op/${receipt.userOpHash}`, + ) return hash } catch (error) { throw new Error( diff --git a/packages/sdk/src/wallet/WalletNamespace.ts b/packages/sdk/src/wallet/WalletNamespace.ts index 3c894021..708e8f03 100644 --- a/packages/sdk/src/wallet/WalletNamespace.ts +++ b/packages/sdk/src/wallet/WalletNamespace.ts @@ -1,5 +1,3 @@ -import type { Hash } from 'viem' - import type { ChainManager } from '@/services/ChainManager.js' import type { LendProvider } from '@/types/lend.js' import { PrivyWalletProvider } from '@/wallet/providers/privy.js' @@ -17,12 +15,12 @@ export class WalletNamespace { withSmartWallet( chainManager: ChainManager, - deployerPrivateKey: Hash, + bundlerUrl: string, lendProvider: LendProvider, ) { this.smartWallet = new SmartWalletProvider( chainManager, - deployerPrivateKey, + bundlerUrl, lendProvider, ) return this diff --git a/packages/sdk/src/wallet/providers/smartWallet.ts b/packages/sdk/src/wallet/providers/smartWallet.ts index 075f0b45..07538250 100644 --- a/packages/sdk/src/wallet/providers/smartWallet.ts +++ b/packages/sdk/src/wallet/providers/smartWallet.ts @@ -1,20 +1,12 @@ -import { chainById } from '@eth-optimism/viem/chains' -import type { Address, Hash } from 'viem' -import { - createWalletClient, - encodeAbiParameters, - http, - parseEventLogs, -} from 'viem' -import { privateKeyToAccount } from 'viem/accounts' +import type { Address } from 'viem' +import { encodeAbiParameters } from 'viem' +import { toCoinbaseSmartAccount } from 'viem/account-abstraction' import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' import { smartWalletFactoryAddress } from '@/constants/addresses.js' import type { ChainManager } from '@/services/ChainManager.js' import type { LendProvider } from '@/types/lend.js' import { SmartWallet } from '@/wallet/SmartWallet.js' -import { toCoinbaseSmartAccount } from 'viem/account-abstraction' -import { SupportedChainId } from '@/constants/supportedChains.js' export class SmartWalletProvider { private chainManager: ChainManager @@ -33,14 +25,13 @@ export class SmartWalletProvider { async createWallet( ownerAddresses: Address[], - bundlerUrl: string, nonce?: bigint, ): Promise { // deploy the wallet on each chain in the chain manager const deployments = await Promise.all( this.chainManager.getSupportedChains().map(async (chainId) => { const publicClient = this.chainManager.getPublicClient(chainId) - const smartAccount = await toCoinbaseSmartAccount({ + const smartAccount = await toCoinbaseSmartAccount({ client: publicClient, owners: ownerAddresses, nonce, @@ -48,8 +39,8 @@ export class SmartWalletProvider { // our own factory at some point we would need to work with them // to update this to allow for other factories version: '1.1', - }) - return smartAccount + }) + return smartAccount }), ) return new SmartWallet( @@ -57,13 +48,12 @@ export class SmartWalletProvider { ownerAddresses, this.chainManager, this.lendProvider, - bundlerUrl, + this.paymasterAndBundlerUrl, ) } async getWallet( initialOwnerAddresses: Address[], - bundlerUrl: string, nonce?: bigint, currentOwnerAddresses?: Address[], ): Promise { @@ -86,7 +76,7 @@ export class SmartWalletProvider { owners, this.chainManager, this.lendProvider, - bundlerUrl, + this.paymasterAndBundlerUrl, ) } } From a36b8eef0023cdba18fe6484d17f829bf9c6e8e9 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 25 Aug 2025 12:12:57 -0700 Subject: [PATCH 06/39] cleaned up smart wallet class and provider --- packages/demo/backend/src/config/env.ts | 2 +- .../demo/backend/src/controllers/wallet.ts | 3 +- packages/demo/backend/src/services/lend.ts | 27 +----- packages/demo/backend/src/services/wallet.ts | 36 ++++--- packages/sdk/src/types/verbs.ts | 2 +- packages/sdk/src/wallet/SmartWallet.ts | 96 +++++++++++++------ .../sdk/src/wallet/providers/smartWallet.ts | 66 ++++++------- 7 files changed, 128 insertions(+), 104 deletions(-) diff --git a/packages/demo/backend/src/config/env.ts b/packages/demo/backend/src/config/env.ts index d780cfa8..629843fe 100644 --- a/packages/demo/backend/src/config/env.ts +++ b/packages/demo/backend/src/config/env.ts @@ -57,5 +57,5 @@ export const env = cleanEnv(process.env, { FAUCET_ADDRESS: str({ default: getFaucetAddressDefault(), }), - BUNDLER_URL: str({ default: 'https://sepolia.bundler.privy.io' }), + BUNDLER_URL: str(), }) diff --git a/packages/demo/backend/src/controllers/wallet.ts b/packages/demo/backend/src/controllers/wallet.ts index 1d1e3454..0f222b3c 100644 --- a/packages/demo/backend/src/controllers/wallet.ts +++ b/packages/demo/backend/src/controllers/wallet.ts @@ -100,9 +100,10 @@ export class WalletController { 404, ) } + const walletAddress = await wallet.getAddress() return c.json({ - address: wallet.address, + address: walletAddress, userId, } satisfies GetWalletResponse) } catch (error) { diff --git a/packages/demo/backend/src/services/lend.ts b/packages/demo/backend/src/services/lend.ts index 0058f304..658513c1 100644 --- a/packages/demo/backend/src/services/lend.ts +++ b/packages/demo/backend/src/services/lend.ts @@ -51,7 +51,8 @@ export async function getVaultBalance( throw new Error(`Wallet not found for user ID: ${walletId}`) } - return await verbs.lend.getVaultBalance(vaultAddress, wallet.address) + const walletAddress = await wallet.getAddress() + return await verbs.lend.getVaultBalance(vaultAddress, walletAddress) } export async function formatVaultResponse( @@ -119,45 +120,21 @@ export async function executeLendTransaction( throw new Error('No transaction data available for execution') } - // const ethBalance = await publicClient.getBalance({ - // address: privyWallet.address, - // }) - - // const gasEstimate = await estimateGasCost( - // publicClient, - // wallet, - // lendTransaction, - // ) - - // if (ethBalance < gasEstimate) { - // throw new Error('Insufficient ETH for gas fees') - // } - if (lendTransaction.transactionData.approval) { - // const approvalSignedTx = await signOnly( - // walletId, - // lendTransaction.transactionData.approval, - // ) await wallet.send( lendTransaction.transactionData.approval, chainId, privyClient as any, walletId, ) - // await publicClient.waitForTransactionReceipt({ hash: approvalHash }) } - // const depositSignedTx = await signOnly( - // walletId, - // lendTransaction.transactionData.deposit, - // ) const depositHash = await wallet.send( lendTransaction.transactionData.deposit, chainId, privyClient as any, walletId, ) - // 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 1e1d975b..1d721ca3 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -32,9 +32,10 @@ export async function createWallet(): Promise<{ const smartWallet = await verbs.wallet.smartWallet!.createWallet([ getAddress(privyWallet.address), ]) + const smartWalletAddress = await smartWallet.getAddress() return { privyAddress: privyWallet.address, - smartWalletAddress: smartWallet.address, + smartWalletAddress, } } @@ -53,9 +54,13 @@ export async function getWallet(userId: string): Promise<{ if (!privyWallet) { throw new Error('Wallet not found') } - const wallet = await verbs.wallet.smartWallet.getWallet([ - getAddress(privyWallet.address), - ]) + const walletAddress = await verbs.wallet.smartWallet.getSmartWalletAddress({ + owners: [getAddress(privyWallet.address)], + }) + const wallet = await verbs.wallet.smartWallet.getWallet({ + walletAddress, + owner: getAddress(privyWallet.address), + }) return { privyWallet, wallet } } @@ -73,11 +78,16 @@ export async function getAllWallets( if (!verbs.wallet.smartWallet) { throw new Error('Smart wallet not configured') } + const walletAddress = + await verbs.wallet.smartWallet.getSmartWalletAddress({ + owners: [getAddress(wallet.address)], + }) return { privyWallet: wallet, - wallet: await verbs.wallet.smartWallet.getWallet([ - getAddress(wallet.address), - ]), + wallet: await verbs.wallet.smartWallet.getWallet({ + walletAddress, + owner: getAddress(wallet.address), + }), } }), ) @@ -106,9 +116,10 @@ export async function getBalance(userId: string): Promise { const vaultBalances = await Promise.all( vaults.map(async (vault) => { try { + const walletAddress = await wallet.getAddress() const vaultBalance = await verbs.lend.getVaultBalance( vault.address, - wallet.address, + walletAddress, ) // Only include vaults with non-zero balances @@ -165,11 +176,12 @@ export async function fundWallet( if (!wallet) { throw new Error('Wallet not found') } + const walletAddress = await wallet.getAddress() if (!isLocalSupersim) { throw new Error(`Wallet fund is coming soon. For now, manually send USDC or ETH to this wallet: -${wallet.address} +${walletAddress} Funding is only available in local development with supersim`) } @@ -198,7 +210,7 @@ Funding is only available in local development with supersim`) address: env.FAUCET_ADDRESS as Address, abi: faucetAbi, functionName: 'dripETH', - args: [wallet.address, amount], + args: [walletAddress, amount], }) privyDripHash = await writeContract(faucetAdminWalletClient, { account: faucetAdminWalletClient.account, @@ -216,7 +228,7 @@ Funding is only available in local development with supersim`) address: env.FAUCET_ADDRESS as Address, abi: faucetAbi, functionName: 'dripERC20', - args: [wallet.address, amount, usdcAddress as Address], + args: [walletAddress, amount, usdcAddress as Address], }) } @@ -232,7 +244,7 @@ Funding is only available in local development with supersim`) return { success: true, tokenType, - to: wallet.address, + to: walletAddress, privyAddress: privyWallet.address, amount: formattedAmount, } diff --git a/packages/sdk/src/types/verbs.ts b/packages/sdk/src/types/verbs.ts index c5b45411..5726a748 100644 --- a/packages/sdk/src/types/verbs.ts +++ b/packages/sdk/src/types/verbs.ts @@ -1,4 +1,4 @@ -import type { Address, Hash } from 'viem' +import type { Address } from 'viem' import type { ChainManager } from '@/services/ChainManager.js' import type { ChainConfig } from '@/types/chain.js' diff --git a/packages/sdk/src/wallet/SmartWallet.ts b/packages/sdk/src/wallet/SmartWallet.ts index bbd06608..59ff1e24 100644 --- a/packages/sdk/src/wallet/SmartWallet.ts +++ b/packages/sdk/src/wallet/SmartWallet.ts @@ -1,6 +1,13 @@ import type { PrivyClient } from '@privy-io/server-auth' -import type { Address, Hash, Hex, Quantity } from 'viem' -import { createPublicClient, encodeFunctionData, erc20Abi, http } from 'viem' +import type { Address, Hash, Hex, LocalAccount, Quantity } from 'viem' +import { + createPublicClient, + encodeFunctionData, + erc20Abi, + http, + pad, +} from 'viem' +import type { WebAuthnAccount } from 'viem/account-abstraction' import { createBundlerClient, toCoinbaseSmartAccount, @@ -9,6 +16,8 @@ import { toAccount } from 'viem/accounts' import { baseSepolia, unichain } from 'viem/chains' import { smartWalletAbi } from '@/abis/smartWallet.js' +import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' +import { smartWalletFactoryAddress } from '@/constants/addresses.js' import type { SupportedChainId } from '@/constants/supportedChains.js' import type { ChainManager } from '@/services/ChainManager.js' import { fetchERC20Balance, fetchETHBalance } from '@/services/tokenBalance.js' @@ -32,11 +41,13 @@ import { * @description Concrete implementation of the Wallet interface */ export class SmartWallet { - public ownerAddresses: Address[] - public address: Address + public owners: Array
+ public ownerIndex?: number + public deploymentAddress?: Address private lendProvider: LendProvider private chainManager: ChainManager private bundlerUrl: string + private nonce?: bigint /** * Create a new wallet instance @@ -44,31 +55,55 @@ export class SmartWallet { * @param verbs - Verbs instance to access configured providers and chain manager */ constructor( - address: Address, - ownerAddresses: Address[], + owners: Array
, chainManager: ChainManager, lendProvider: LendProvider, bundlerUrl: string, + deploymentAddress?: Address, + ownerIndex?: number, + nonce?: bigint, ) { - this.address = address - this.ownerAddresses = ownerAddresses + this.owners = owners + this.ownerIndex = ownerIndex + this.deploymentAddress = deploymentAddress this.chainManager = chainManager this.lendProvider = lendProvider this.bundlerUrl = bundlerUrl + this.nonce = nonce + } + + async getAddress() { + if (this.deploymentAddress) return this.deploymentAddress + + const owners_bytes = this.owners.map((owner) => { + if (typeof owner === 'string') return pad(owner) + if (owner.type === 'webAuthn') return owner.publicKey + throw new Error('invalid owner type') + }) + + // Factory is the same accross all chains, so we can use the first chain to get the wallet address + const publicClient = this.chainManager.getPublicClient( + this.chainManager.getSupportedChains()[0], + ) + const smartWalletAddress = await publicClient.readContract({ + abi: smartWalletFactoryAbi, + address: smartWalletFactoryAddress, + functionName: 'getAddress', + args: [owners_bytes, this.nonce || 0n], + }) + return smartWalletAddress } async getCoinbaseSmartAccount( chainId: SupportedChainId, - privyAccount: any, - nonce?: bigint, + privyAccount: LocalAccount, ): Promise> { return toCoinbaseSmartAccount({ - client: createPublicClient({ - chain: baseSepolia, - transport: http(this.bundlerUrl), - }), + address: this.deploymentAddress, + ownerIndex: this.ownerIndex, + client: this.chainManager.getPublicClient(chainId), owners: [privyAccount], - nonce, + nonce: this.nonce, version: '1.1', }) } @@ -78,13 +113,13 @@ export class SmartWallet { * @returns Promise resolving to array of asset balances */ async getBalance(): Promise { - console.log('getting balance for address', this.address) + const address = await this.getAddress() const tokenBalancePromises = Object.values(SUPPORTED_TOKENS).map( async (token) => { - return fetchERC20Balance(this.chainManager, this.address, token) + return fetchERC20Balance(this.chainManager, address, token) }, ) - const ethBalancePromise = fetchETHBalance(this.chainManager, this.address) + const ethBalancePromise = fetchETHBalance(this.chainManager, address) return Promise.all([ethBalancePromise, ...tokenBalancePromises]) } @@ -113,11 +148,12 @@ export class SmartWallet { asset, chainId, ) + const address = await this.getAddress() // Set receiver to wallet address if not specified const lendOptions: LendOptions = { ...options, - receiver: options?.receiver || this.address, + receiver: options?.receiver || address, } const result = await this.lendProvider.deposit( @@ -141,7 +177,6 @@ export class SmartWallet { async getTxParams( transactionData: TransactionData, chainId: SupportedChainId, - ownerIndex: number = 0, ): Promise<{ /** The address the transaction is sent from. Must be hexadecimal formatted. */ from?: Hex @@ -169,11 +204,11 @@ export class SmartWallet { const executeCallData = this.execute(transactionData) const publicClient = this.chainManager.getPublicClient(chainId) + const address = await this.getAddress() // Estimate gas limit const gasLimit = await publicClient.estimateGas({ - account: this.ownerAddresses[ownerIndex], - to: this.address, + to: address, data: executeCallData, value: BigInt(transactionData.value), }) @@ -182,13 +217,13 @@ export class SmartWallet { const feeData = await publicClient.estimateFeesPerGas() // Get current nonce for the wallet - manual management since Privy isn't handling it properly - const nonce = await publicClient.getTransactionCount({ - address: this.ownerAddresses[ownerIndex], - blockTag: 'pending', // Use pending to get the next nonce including any pending txs - }) + // const nonce = await publicClient.getTransactionCount({ + // address: this.ownerAddresses[ownerIndex], + // blockTag: 'pending', // Use pending to get the next nonce including any pending txs + // }) return { - to: this.address, + to: address, data: executeCallData, value: transactionData.value.toString(16) as `0x${string}`, chainId: `0x${chainId.toString(16)}`, @@ -196,20 +231,19 @@ export class SmartWallet { gasLimit: `0x${gasLimit.toString(16)}`, maxFeePerGas: `0x${(feeData.maxFeePerGas || BigInt(1000000000)).toString(16)}`, // fallback to 1 gwei maxPriorityFeePerGas: `0x${(feeData.maxPriorityFeePerGas || BigInt(100000000)).toString(16)}`, // fallback to 0.1 gwei - nonce: `0x${nonce.toString(16)}`, // Explicitly provide the correct nonce + // nonce: `0x${nonce.toString(16)}`, // Explicitly provide the correct nonce } } async estimateGas( transactionData: TransactionData, chainId: SupportedChainId, - ownerIndex: number = 0, ): Promise { const executeCallData = this.execute(transactionData) const publicClient = this.chainManager.getPublicClient(chainId) + const address = await this.getAddress() return publicClient.estimateGas({ - account: this.ownerAddresses[ownerIndex], - to: this.address, + to: address, data: executeCallData, value: BigInt(transactionData.value), }) diff --git a/packages/sdk/src/wallet/providers/smartWallet.ts b/packages/sdk/src/wallet/providers/smartWallet.ts index 07538250..8dd59a57 100644 --- a/packages/sdk/src/wallet/providers/smartWallet.ts +++ b/packages/sdk/src/wallet/providers/smartWallet.ts @@ -1,6 +1,6 @@ import type { Address } from 'viem' -import { encodeAbiParameters } from 'viem' -import { toCoinbaseSmartAccount } from 'viem/account-abstraction' +import { pad } from 'viem' +import { type WebAuthnAccount } from 'viem/account-abstraction' import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' import { smartWalletFactoryAddress } from '@/constants/addresses.js' @@ -24,59 +24,59 @@ export class SmartWalletProvider { } async createWallet( - ownerAddresses: Address[], + owners: Array
, nonce?: bigint, ): Promise { - // deploy the wallet on each chain in the chain manager - const deployments = await Promise.all( - this.chainManager.getSupportedChains().map(async (chainId) => { - const publicClient = this.chainManager.getPublicClient(chainId) - const smartAccount = await toCoinbaseSmartAccount({ - client: publicClient, - owners: ownerAddresses, - nonce, - // viem only supports the factory deployed by cb. if we wanted - // our own factory at some point we would need to work with them - // to update this to allow for other factories - version: '1.1', - }) - return smartAccount - }), - ) return new SmartWallet( - deployments[0].address, - ownerAddresses, + owners, this.chainManager, this.lendProvider, this.paymasterAndBundlerUrl, + undefined, + undefined, + nonce, ) } - async getWallet( - initialOwnerAddresses: Address[], - nonce?: bigint, - currentOwnerAddresses?: Address[], - ): Promise { + async getSmartWalletAddress(params: { + owners: Array
+ nonce?: bigint + }) { + const { owners, nonce = 0n } = params + const owners_bytes = owners.map((owner) => { + if (typeof owner === 'string') return pad(owner) + if (owner.type === 'webAuthn') return owner.publicKey + throw new Error('invalid owner type') + }) + // Factory is the same accross all chains, so we can use the first chain to get the wallet address const publicClient = this.chainManager.getPublicClient( this.chainManager.getSupportedChains()[0], ) - const encodedOwners = initialOwnerAddresses.map((ownerAddress) => - encodeAbiParameters([{ type: 'address' }], [ownerAddress]), - ) const smartWalletAddress = await publicClient.readContract({ abi: smartWalletFactoryAbi, address: smartWalletFactoryAddress, functionName: 'getAddress', - args: [encodedOwners, nonce || 0n], + args: [owners_bytes, nonce], }) - const owners = currentOwnerAddresses || initialOwnerAddresses + return smartWalletAddress + } + + async getWallet(params: { + walletAddress: Address + owner: Address | WebAuthnAccount + ownerIndex?: number + nonce?: bigint + }): Promise { + const { walletAddress, owner, ownerIndex, nonce } = params return new SmartWallet( - smartWalletAddress, - owners, + [owner], this.chainManager, this.lendProvider, this.paymasterAndBundlerUrl, + walletAddress, + ownerIndex, + nonce, ) } } From 6b8c1c87cfcc94f3abc752cb3537488d3248d689 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 25 Aug 2025 12:18:26 -0700 Subject: [PATCH 07/39] remove unused functions on SmartWallet --- packages/sdk/src/wallet/SmartWallet.ts | 145 +------------------------ 1 file changed, 1 insertion(+), 144 deletions(-) diff --git a/packages/sdk/src/wallet/SmartWallet.ts b/packages/sdk/src/wallet/SmartWallet.ts index 59ff1e24..d7402c60 100644 --- a/packages/sdk/src/wallet/SmartWallet.ts +++ b/packages/sdk/src/wallet/SmartWallet.ts @@ -1,5 +1,5 @@ import type { PrivyClient } from '@privy-io/server-auth' -import type { Address, Hash, Hex, LocalAccount, Quantity } from 'viem' +import type { Address, Hash, LocalAccount } from 'viem' import { createPublicClient, encodeFunctionData, @@ -15,7 +15,6 @@ import { import { toAccount } from 'viem/accounts' import { baseSepolia, unichain } from 'viem/chains' -import { smartWalletAbi } from '@/abis/smartWallet.js' import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' import { smartWalletFactoryAddress } from '@/constants/addresses.js' import type { SupportedChainId } from '@/constants/supportedChains.js' @@ -166,89 +165,6 @@ export class SmartWallet { return result } - execute(transactionData: TransactionData): Hash { - return encodeFunctionData({ - abi: smartWalletAbi, - functionName: 'execute', - args: [transactionData.to, transactionData.value, transactionData.data], - }) - } - - async getTxParams( - transactionData: TransactionData, - chainId: SupportedChainId, - ): Promise<{ - /** The address the transaction is sent from. Must be hexadecimal formatted. */ - from?: Hex - /** Destination address of the transaction. */ - to?: Hex - /** The nonce to be used for the transaction (hexadecimal or number). */ - nonce?: Quantity - /** (optional) The chain ID of network your transaction will be sent on. */ - chainId?: Quantity - /** (optional) Data to send to the receiving address, especially when calling smart contracts. Must be hexadecimal formatted. */ - data?: Hex - /** (optional) The value (in wei) be sent with the transaction (hexadecimal or number). */ - value?: Quantity - /** (optional) The EIP-2718 transction type (e.g. `2` for EIP-1559 transactions). */ - type?: 0 | 1 | 2 - /** (optional) The max units of gas that can be used by this transaction (hexadecimal or number). */ - gasLimit?: Quantity - /** (optional) The price (in wei) per unit of gas for this transaction (hexadecimal or number), for use in non EIP-1559 transactions (type 0 or 1). */ - gasPrice?: Quantity - /** (optional) The maxFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ - maxFeePerGas?: Quantity - /** (optional) The maxPriorityFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ - maxPriorityFeePerGas?: Quantity - }> { - const executeCallData = this.execute(transactionData) - - const publicClient = this.chainManager.getPublicClient(chainId) - const address = await this.getAddress() - - // Estimate gas limit - const gasLimit = await publicClient.estimateGas({ - to: address, - data: executeCallData, - value: BigInt(transactionData.value), - }) - - // Get current gas price and fee data - const feeData = await publicClient.estimateFeesPerGas() - - // Get current nonce for the wallet - manual management since Privy isn't handling it properly - // const nonce = await publicClient.getTransactionCount({ - // address: this.ownerAddresses[ownerIndex], - // blockTag: 'pending', // Use pending to get the next nonce including any pending txs - // }) - - return { - to: address, - data: executeCallData, - value: transactionData.value.toString(16) as `0x${string}`, - chainId: `0x${chainId.toString(16)}`, - type: 2, // EIP-1559 - gasLimit: `0x${gasLimit.toString(16)}`, - maxFeePerGas: `0x${(feeData.maxFeePerGas || BigInt(1000000000)).toString(16)}`, // fallback to 1 gwei - maxPriorityFeePerGas: `0x${(feeData.maxPriorityFeePerGas || BigInt(100000000)).toString(16)}`, // fallback to 0.1 gwei - // nonce: `0x${nonce.toString(16)}`, // Explicitly provide the correct nonce - } - } - - async estimateGas( - transactionData: TransactionData, - chainId: SupportedChainId, - ): Promise { - const executeCallData = this.execute(transactionData) - const publicClient = this.chainManager.getPublicClient(chainId) - const address = await this.getAddress() - return publicClient.estimateGas({ - to: address, - data: executeCallData, - value: BigInt(transactionData.value), - }) - } - /** * Send a signed transaction * @description Sends a pre-signed transaction to the network @@ -267,7 +183,6 @@ export class SmartWallet { id: privyWalletId, }) const signerAddress = privyWallet.address - console.log('Signer:', signerAddress) const privyAccount = toAccount({ address: signerAddress as Address, async signMessage({ message }) { @@ -332,64 +247,6 @@ export class SmartWallet { }, } const calls = [transactionData] - // const userOperation = await bundlerClient.prepareUserOperation({ - // account, - // calls, - // paymaster: true, - // }) - // const address = await account.getAddress() - // console.log('account address', address) - // console.log('this address', this.address) - // const userOpHash = getUserOperationHash({ - // chainId, - // entryPointAddress: account.entryPoint.address, - // entryPointVersion: account.entryPoint.version, - // userOperation: {...userOperation, sender: address}, - // }) - // console.log("UserOperation being signed:", JSON.stringify(userOperation, null, 2)) - // console.log("UserOp hash:", userOpHash) - // console.log("Signing initCode?", !!userOperation.initCode) - // console.log("initCode", userOperation.initCode) - // console.log("Signing callData?", !!userOperation.callData) - // console.log("Signing callData?", !!userOperation.callData) - // console.log('entry point version', account.entryPoint.version) - // console.log('entry point address', account.entryPoint.address) - - // console.log("Expected owner:", "0x62F5E6630F48077a8C96CA6BD5Dc561aB163465C"); - // console.log('owner addresses', this.ownerAddresses) - // const signedMessage = await privyClient.walletApi.ethereum.signMessage({ walletId: privyWalletId, message: userOpHash }) - // const signedMessage = await privyClient.walletApi.ethereum.secp256k1Sign({ walletId: privyWalletId, hash: userOpHash }) - - // const recoveredAddress = await recoverAddress({ - // hash: userOpHash, - // signature: signedMessage.signature as Hash // raw signature without wrapper - // }); - - // console.log("Recovered address:", recoveredAddress); - // console.log("Expected owner:", "0x62F5E6630F48077a8C96CA6BD5Dc561aB163465C"); - // const signatureData = processSignature(signedMessage.signature as Hash) - - // You need to wrap it in SignatureWrapper format - // const signature = encodeAbiParameters( - // [{ - // type: 'tuple', - // components: [ - // { name: 'ownerIndex', type: 'uint8' }, - // { name: 'signatureData', type: 'bytes' } - // ] - // }], - // [{ ownerIndex: 0, signatureData: signedMessage.signature as Hash }] - // ); - // const userOp = await bundlerClient.sendUserOperation({ - // account, - // calls, - // signature: signedMessage.signature as Hash, - // paymaster: true - // }); - // const hash = await bundlerClient.sendUserOperation({ - // ...userOperation, - // // signature, - // }) const hash = await bundlerClient.sendUserOperation({ account, calls, From d05354bcbea59af6f410fb535f60d4d9ebad88f0 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 25 Aug 2025 13:16:12 -0700 Subject: [PATCH 08/39] refactored smart wallet to not contain privy --- packages/demo/backend/src/controllers/lend.ts | 8 -- packages/demo/backend/src/services/lend.ts | 11 +-- packages/demo/backend/src/services/wallet.ts | 20 ++-- packages/sdk/src/wallet/PrivyWallet.ts | 38 +++++++- packages/sdk/src/wallet/SmartWallet.ts | 95 ++++++------------- .../sdk/src/wallet/providers/smartWallet.ts | 11 ++- 6 files changed, 86 insertions(+), 97 deletions(-) diff --git a/packages/demo/backend/src/controllers/lend.ts b/packages/demo/backend/src/controllers/lend.ts index 1bd3553d..f19afce0 100644 --- a/packages/demo/backend/src/controllers/lend.ts +++ b/packages/demo/backend/src/controllers/lend.ts @@ -1,11 +1,8 @@ -import { PrivyClient } from '@privy-io/server-auth' import type { Context } from 'hono' import type { Address } from 'viem' import { baseSepolia } from 'viem/chains' import { z } from 'zod' -import { env } from '@/config/env.js' - import { validateRequest } from '../helpers/validation.js' import * as lendService from '../services/lend.js' @@ -121,10 +118,6 @@ export class LendController { const { body: { walletId, amount, token }, } = validation.data - const privyClient = new PrivyClient( - env.PRIVY_APP_ID, - env.PRIVY_APP_SECRET, - ) const lendTransaction = await lendService.deposit( walletId, amount, @@ -135,7 +128,6 @@ export class LendController { walletId, lendTransaction, baseSepolia.id, - privyClient, ) return c.json({ diff --git a/packages/demo/backend/src/services/lend.ts b/packages/demo/backend/src/services/lend.ts index 658513c1..5201cba9 100644 --- a/packages/demo/backend/src/services/lend.ts +++ b/packages/demo/backend/src/services/lend.ts @@ -3,7 +3,6 @@ import type { LendVaultInfo, SupportedChainId, } from '@eth-optimism/verbs-sdk' -import type { PrivyClient } from '@privy-io/server-auth' import type { Address } from 'viem' import { getVerbs } from '../config/verbs.js' @@ -108,7 +107,6 @@ export async function executeLendTransaction( walletId: string, lendTransaction: LendTransaction, chainId: SupportedChainId, - privyClient: PrivyClient, ): Promise { const { wallet } = await getWallet(walletId) @@ -121,19 +119,12 @@ export async function executeLendTransaction( } if (lendTransaction.transactionData.approval) { - await wallet.send( - lendTransaction.transactionData.approval, - chainId, - privyClient as any, - walletId, - ) + await wallet.send(lendTransaction.transactionData.approval, chainId) } const depositHash = await wallet.send( lendTransaction.transactionData.deposit, chainId, - privyClient as any, - walletId, ) return { ...lendTransaction, hash: depositHash } diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index 1d721ca3..907f2fc5 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -29,9 +29,11 @@ export async function createWallet(): Promise<{ }> { const verbs = getVerbs() const privyWallet = await verbs.wallet.privy!.createWallet() - const smartWallet = await verbs.wallet.smartWallet!.createWallet([ - getAddress(privyWallet.address), - ]) + const signer = await privyWallet.signer() + const smartWallet = await verbs.wallet.smartWallet!.createWallet( + [getAddress(privyWallet.address)], + signer, + ) const smartWalletAddress = await smartWallet.getAddress() return { privyAddress: privyWallet.address, @@ -54,12 +56,13 @@ export async function getWallet(userId: string): Promise<{ if (!privyWallet) { throw new Error('Wallet not found') } + const signer = await privyWallet.signer() const walletAddress = await verbs.wallet.smartWallet.getSmartWalletAddress({ owners: [getAddress(privyWallet.address)], }) const wallet = await verbs.wallet.smartWallet.getWallet({ walletAddress, - owner: getAddress(privyWallet.address), + signer, }) return { privyWallet, wallet } } @@ -74,19 +77,20 @@ export async function getAllWallets( } const privyWallets = await verbs.wallet.privy.getAllWallets(options) return Promise.all( - privyWallets.map(async (wallet) => { + privyWallets.map(async (privyWallet) => { if (!verbs.wallet.smartWallet) { throw new Error('Smart wallet not configured') } const walletAddress = await verbs.wallet.smartWallet.getSmartWalletAddress({ - owners: [getAddress(wallet.address)], + owners: [getAddress(privyWallet.address)], }) + const signer = await privyWallet.signer() return { - privyWallet: wallet, + privyWallet, wallet: await verbs.wallet.smartWallet.getWallet({ walletAddress, - owner: getAddress(wallet.address), + signer, }), } }), diff --git a/packages/sdk/src/wallet/PrivyWallet.ts b/packages/sdk/src/wallet/PrivyWallet.ts index 4265b125..3424339a 100644 --- a/packages/sdk/src/wallet/PrivyWallet.ts +++ b/packages/sdk/src/wallet/PrivyWallet.ts @@ -1,4 +1,5 @@ -import type { Address, Hex, Quantity } from 'viem' +import type { Address, Hash, Hex, LocalAccount, Quantity } from 'viem' +import { toAccount } from 'viem/accounts' import type { ChainManager } from '@/services/ChainManager.js' @@ -31,6 +32,41 @@ export class PrivyWallet { this.address = address } + async signer(): Promise { + const privy = this.privyProvider.privy + const walletId = this.walletId + const privyWallet = await privy.walletApi.getWallet({ + id: walletId, + }) + const signerAddress = privyWallet.address + + return toAccount({ + address: signerAddress as Address, + async signMessage({ message }) { + const signed = await privy.walletApi.ethereum.signMessage({ + walletId, + message: message.toString(), + }) + return signed.signature as Hash + }, + async sign(parameters) { + const signed = await privy.walletApi.ethereum.secp256k1Sign({ + walletId, + hash: parameters.hash, + }) + return signed.signature as Hash + }, + async signTransaction() { + // Implement if needed + throw new Error('Not implemented') + }, + async signTypedData() { + // Implement if needed + throw new Error('Not implemented') + }, + }) + } + /** * Sign a transaction without sending it * @description Signs a transaction using Privy's wallet API but doesn't send it diff --git a/packages/sdk/src/wallet/SmartWallet.ts b/packages/sdk/src/wallet/SmartWallet.ts index d7402c60..a45b0c15 100644 --- a/packages/sdk/src/wallet/SmartWallet.ts +++ b/packages/sdk/src/wallet/SmartWallet.ts @@ -1,4 +1,3 @@ -import type { PrivyClient } from '@privy-io/server-auth' import type { Address, Hash, LocalAccount } from 'viem' import { createPublicClient, @@ -12,7 +11,6 @@ import { createBundlerClient, toCoinbaseSmartAccount, } from 'viem/account-abstraction' -import { toAccount } from 'viem/accounts' import { baseSepolia, unichain } from 'viem/chains' import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' @@ -40,9 +38,10 @@ import { * @description Concrete implementation of the Wallet interface */ export class SmartWallet { - public owners: Array
- public ownerIndex?: number - public deploymentAddress?: Address + private owners: Array
+ private signer: LocalAccount + private ownerIndex?: number + private deploymentAddress?: Address private lendProvider: LendProvider private chainManager: ChainManager private bundlerUrl: string @@ -55,6 +54,7 @@ export class SmartWallet { */ constructor( owners: Array
, + signer: LocalAccount, chainManager: ChainManager, lendProvider: LendProvider, bundlerUrl: string, @@ -63,6 +63,7 @@ export class SmartWallet { nonce?: bigint, ) { this.owners = owners + this.signer = signer this.ownerIndex = ownerIndex this.deploymentAddress = deploymentAddress this.chainManager = chainManager @@ -95,13 +96,12 @@ export class SmartWallet { async getCoinbaseSmartAccount( chainId: SupportedChainId, - privyAccount: LocalAccount, - ): Promise> { + ): ReturnType { return toCoinbaseSmartAccount({ address: this.deploymentAddress, ownerIndex: this.ownerIndex, client: this.chainManager.getPublicClient(chainId), - owners: [privyAccount], + owners: [this.signer], nonce: this.nonce, version: '1.1', }) @@ -175,40 +175,9 @@ export class SmartWallet { async send( transactionData: TransactionData, chainId: SupportedChainId, - privyClient: PrivyClient, - privyWalletId: string, ): Promise { try { - const privyWallet = await privyClient.walletApi.getWallet({ - id: privyWalletId, - }) - const signerAddress = privyWallet.address - const privyAccount = toAccount({ - address: signerAddress as Address, - async signMessage({ message }) { - const signed = await privyClient.walletApi.ethereum.signMessage({ - walletId: privyWalletId, - message: message.toString(), - }) - return signed.signature as Hash - }, - async sign(parameters) { - const signed = await privyClient.walletApi.ethereum.secp256k1Sign({ - walletId: privyWalletId, - hash: parameters.hash, - }) - return signed.signature as Hash - }, - async signTransaction() { - // Implement if needed - throw new Error('Not implemented') - }, - async signTypedData() { - // Implement if needed - throw new Error('Not implemented') - }, - }) - const account = await this.getCoinbaseSmartAccount(chainId, privyAccount) + const account = await this.getCoinbaseSmartAccount(chainId) const client = createPublicClient({ chain: baseSepolia, transport: http(this.bundlerUrl), @@ -220,32 +189,26 @@ export class SmartWallet { chain: baseSepolia, }) // Pads the preVerificationGas (or any other gas limits you might want) to ensure your UserOperation lands onchain - account.userOperation = { - estimateGas: async (userOperation) => { - try { - const estimate = await bundlerClient.estimateUserOperationGas( - userOperation as any, - ) - console.log('estimate succeeded', estimate) - // adjust preVerification upward - estimate.preVerificationGas = estimate.preVerificationGas * 2n - // return estimate; - return { - ...estimate, - preVerificationGas: estimate.preVerificationGas * 2n, - verificationGasLimit: estimate.verificationGasLimit * 2n, // Most important for AA23 - callGasLimit: estimate.callGasLimit * 2n, - } - } catch (error) { - console.error('Failed to estimate gas:', error) - return { - preVerificationGas: 200000n, - verificationGasLimit: 800000n, // High limit for complex validation - callGasLimit: 200000n, - } - } - }, - } + // account.userOperation = { + // estimateGas: async (userOperation) => { + // try { + // const estimate = await bundlerClient.estimateUserOperationGas( + // userOperation, + // ) + // // adjust preVerification upward + // estimate.preVerificationGas = estimate.preVerificationGas * 2n + // return { + // ...estimate, + // preVerificationGas: estimate.preVerificationGas * 2n, + // verificationGasLimit: estimate.verificationGasLimit * 2n, + // callGasLimit: estimate.callGasLimit * 2n, + // } + // } catch (error) { + // console.error('Failed to estimate gas:', error) + // throw error + // } + // }, + // } const calls = [transactionData] const hash = await bundlerClient.sendUserOperation({ account, diff --git a/packages/sdk/src/wallet/providers/smartWallet.ts b/packages/sdk/src/wallet/providers/smartWallet.ts index 8dd59a57..c7393787 100644 --- a/packages/sdk/src/wallet/providers/smartWallet.ts +++ b/packages/sdk/src/wallet/providers/smartWallet.ts @@ -1,4 +1,4 @@ -import type { Address } from 'viem' +import type { Address, LocalAccount } from 'viem' import { pad } from 'viem' import { type WebAuthnAccount } from 'viem/account-abstraction' @@ -25,10 +25,12 @@ export class SmartWalletProvider { async createWallet( owners: Array
, + signer: LocalAccount, nonce?: bigint, ): Promise { return new SmartWallet( owners, + signer, this.chainManager, this.lendProvider, this.paymasterAndBundlerUrl, @@ -64,13 +66,14 @@ export class SmartWalletProvider { async getWallet(params: { walletAddress: Address - owner: Address | WebAuthnAccount + signer: LocalAccount ownerIndex?: number nonce?: bigint }): Promise { - const { walletAddress, owner, ownerIndex, nonce } = params + const { walletAddress, signer, ownerIndex, nonce } = params return new SmartWallet( - [owner], + [signer.address], + signer, this.chainManager, this.lendProvider, this.paymasterAndBundlerUrl, From 2095e9b25b0c70987e42bc8a8935efb248043e24 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 25 Aug 2025 13:27:45 -0700 Subject: [PATCH 09/39] clean up rpc urls --- packages/demo/backend/src/config/env.ts | 3 ++- packages/demo/backend/src/config/verbs.ts | 11 ++++++----- packages/demo/backend/src/services/wallet.ts | 6 +++--- packages/sdk/src/types/verbs.ts | 15 ++++++++++++--- packages/sdk/src/verbs.test.ts | 10 +++++----- packages/sdk/src/verbs.ts | 10 +++++----- 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/packages/demo/backend/src/config/env.ts b/packages/demo/backend/src/config/env.ts index 629843fe..747181f4 100644 --- a/packages/demo/backend/src/config/env.ts +++ b/packages/demo/backend/src/config/env.ts @@ -45,7 +45,8 @@ export const env = cleanEnv(process.env, { PRIVY_APP_ID: str({ devDefault: 'dummy' }), PRIVY_APP_SECRET: str({ devDefault: 'dummy' }), LOCAL_DEV: bool({ default: false }), - RPC_URL: str({ default: 'http://127.0.0.1:9545' }), + BASE_SEPOLIA_RPC_URL: str({ default: undefined }), + UNICHAIN_RPC_URL: str({ default: undefined }), FAUCET_ADMIN_PRIVATE_KEY: str({ default: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', diff --git a/packages/demo/backend/src/config/verbs.ts b/packages/demo/backend/src/config/verbs.ts index f7d1b0f6..23f8e078 100644 --- a/packages/demo/backend/src/config/verbs.ts +++ b/packages/demo/backend/src/config/verbs.ts @@ -11,7 +11,7 @@ let verbsInstance: Verbs export function createVerbsConfig(): VerbsConfig { return { - wallet: { + privyConfig: { type: 'privy', appId: env.PRIVY_APP_ID, appSecret: env.PRIVY_APP_SECRET, @@ -22,15 +22,16 @@ export function createVerbsConfig(): VerbsConfig { chains: [ { chainId: unichain.id, - rpcUrl: unichain.rpcUrls.default.http[0], - // rpcUrl: env.RPC_URL, + rpcUrl: env.UNICHAIN_RPC_URL || unichain.rpcUrls.default.http[0], }, { chainId: baseSepolia.id, - rpcUrl: env.RPC_URL, + rpcUrl: env.BASE_SEPOLIA_RPC_URL || baseSepolia.rpcUrls.default.http[0], }, ], - bundlerUrl: env.BUNDLER_URL, + smartWalletConfig: { + bundlerUrl: env.BUNDLER_URL, + }, } } diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index 907f2fc5..6daaa164 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -174,7 +174,7 @@ export async function fundWallet( amount: string }> { // TODO: do this a better way - const isLocalSupersim = env.RPC_URL === 'http://127.0.0.1:9545' + const isLocalSupersim = env.LOCAL_DEV const { wallet, privyWallet } = await getWallet(userId) if (!wallet) { @@ -192,13 +192,13 @@ Funding is only available in local development with supersim`) const faucetAdminWalletClient = createWalletClient({ chain: unichain, - transport: http(env.RPC_URL), + transport: http(env.UNICHAIN_RPC_URL), account: privateKeyToAccount(env.FAUCET_ADMIN_PRIVATE_KEY as Hex), }) const publicClient = createPublicClient({ chain: unichain, - transport: http(env.RPC_URL), + transport: http(env.UNICHAIN_RPC_URL), }) let dripHash: `0x${string}` diff --git a/packages/sdk/src/types/verbs.ts b/packages/sdk/src/types/verbs.ts index 5726a748..6a87ccce 100644 --- a/packages/sdk/src/types/verbs.ts +++ b/packages/sdk/src/types/verbs.ts @@ -51,13 +51,13 @@ export interface VerbsInterface { */ export interface VerbsConfig { /** Wallet provider configuration */ - wallet: WalletConfig + privyConfig?: PrivyWalletConfig + /** Smart wallet provider configuration */ + smartWalletConfig?: SmartWalletConfig /** Lending provider configuration (optional) */ lend?: LendConfig /** Chains to use for the SDK */ chains?: ChainConfig[] - /** Bundler URL for the wallet */ - bundlerUrl?: string } /** @@ -78,3 +78,12 @@ export interface PrivyWalletConfig { /** Privy app secret */ appSecret: string } + +/** + * Smart wallet provider configuration + * @description Configuration for smart wallet provider + */ +export interface SmartWalletConfig { + /** Bundler URL for the smart wallet */ + bundlerUrl: string +} diff --git a/packages/sdk/src/verbs.test.ts b/packages/sdk/src/verbs.test.ts index 957268c2..af5b6cb5 100644 --- a/packages/sdk/src/verbs.test.ts +++ b/packages/sdk/src/verbs.test.ts @@ -16,7 +16,7 @@ describe('Verbs SDK - System Tests', () => { type: 'morpho', defaultSlippage: 50, }, - wallet: { + privyConfig: { type: 'privy', appId: 'test-app-id', appSecret: 'test-app-secret', @@ -76,7 +76,7 @@ describe('Verbs SDK - System Tests', () => { type: 'morpho', defaultSlippage: 50, }, - wallet: { + privyConfig: { type: 'privy', appId: 'test-app-id', appSecret: 'test-app-secret', @@ -107,7 +107,7 @@ describe('Verbs SDK - System Tests', () => { type: 'morpho', defaultSlippage: 50, }, - wallet: { + privyConfig: { type: 'privy', appId: 'test-app-id', appSecret: 'test-app-secret', @@ -128,7 +128,7 @@ describe('Verbs SDK - System Tests', () => { type: 'morpho', defaultSlippage: 50, }, - wallet: { + privyConfig: { type: 'privy', appId: 'test-app-id', appSecret: 'test-app-secret', @@ -147,7 +147,7 @@ describe('Verbs SDK - System Tests', () => { type: 'morpho', defaultSlippage: 50, }, - wallet: { + privyConfig: { type: 'privy', appId: 'test-app-id', appSecret: 'test-app-secret', diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index 8b6eddc3..8b47b059 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -69,17 +69,17 @@ export class Verbs { */ export function initVerbs(config: VerbsConfig) { const verbs = new Verbs(config) - if (config.wallet) { + if (config.privyConfig) { verbs.wallet.withPrivy( - config.wallet.appId, - config.wallet.appSecret, + config.privyConfig.appId, + config.privyConfig.appSecret, verbs.chainManager, ) } - if (config.bundlerUrl) { + if (config.smartWalletConfig) { verbs.wallet.withSmartWallet( verbs.chainManager, - config.bundlerUrl, + config.smartWalletConfig.bundlerUrl, verbs.lend, ) } From f8de7d1a258ebc93c4ba90eed9300d30e07266a2 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 25 Aug 2025 13:37:47 -0700 Subject: [PATCH 10/39] nit --- packages/sdk/src/wallet/SmartWallet.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/packages/sdk/src/wallet/SmartWallet.ts b/packages/sdk/src/wallet/SmartWallet.ts index a45b0c15..07923069 100644 --- a/packages/sdk/src/wallet/SmartWallet.ts +++ b/packages/sdk/src/wallet/SmartWallet.ts @@ -188,27 +188,6 @@ export class SmartWallet { transport: http(this.bundlerUrl), chain: baseSepolia, }) - // Pads the preVerificationGas (or any other gas limits you might want) to ensure your UserOperation lands onchain - // account.userOperation = { - // estimateGas: async (userOperation) => { - // try { - // const estimate = await bundlerClient.estimateUserOperationGas( - // userOperation, - // ) - // // adjust preVerification upward - // estimate.preVerificationGas = estimate.preVerificationGas * 2n - // return { - // ...estimate, - // preVerificationGas: estimate.preVerificationGas * 2n, - // verificationGasLimit: estimate.verificationGasLimit * 2n, - // callGasLimit: estimate.callGasLimit * 2n, - // } - // } catch (error) { - // console.error('Failed to estimate gas:', error) - // throw error - // } - // }, - // } const calls = [transactionData] const hash = await bundlerClient.sendUserOperation({ account, From 7cc09f18969713957a7415fe54eb575d586fe27d Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 25 Aug 2025 13:39:32 -0700 Subject: [PATCH 11/39] remove unused env var --- packages/demo/backend/src/config/env.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/demo/backend/src/config/env.ts b/packages/demo/backend/src/config/env.ts index 747181f4..e58b7305 100644 --- a/packages/demo/backend/src/config/env.ts +++ b/packages/demo/backend/src/config/env.ts @@ -51,10 +51,6 @@ export const env = cleanEnv(process.env, { default: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', }), - PRIVATE_KEY: str({ - default: - '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', - }), FAUCET_ADDRESS: str({ default: getFaucetAddressDefault(), }), From 66c40f17a9e01e5cebcab42cdb724f253b9711bc Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 25 Aug 2025 13:49:10 -0700 Subject: [PATCH 12/39] remove nonce from getWallet and make synchronous --- packages/demo/backend/src/services/wallet.ts | 4 ++-- .../sdk/src/wallet/providers/smartWallet.ts | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index 6daaa164..bc933163 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -60,7 +60,7 @@ export async function getWallet(userId: string): Promise<{ const walletAddress = await verbs.wallet.smartWallet.getSmartWalletAddress({ owners: [getAddress(privyWallet.address)], }) - const wallet = await verbs.wallet.smartWallet.getWallet({ + const wallet = verbs.wallet.smartWallet.getWallet({ walletAddress, signer, }) @@ -88,7 +88,7 @@ export async function getAllWallets( const signer = await privyWallet.signer() return { privyWallet, - wallet: await verbs.wallet.smartWallet.getWallet({ + wallet: verbs.wallet.smartWallet.getWallet({ walletAddress, signer, }), diff --git a/packages/sdk/src/wallet/providers/smartWallet.ts b/packages/sdk/src/wallet/providers/smartWallet.ts index c7393787..baea1b40 100644 --- a/packages/sdk/src/wallet/providers/smartWallet.ts +++ b/packages/sdk/src/wallet/providers/smartWallet.ts @@ -64,13 +64,21 @@ export class SmartWalletProvider { return smartWalletAddress } - async getWallet(params: { + /** + * Get an existing smart wallet instance + * @description Creates a SmartWallet instance for an already deployed wallet. + * Use this when you know the wallet address and want to interact with it. + * @param params.walletAddress - Address of the deployed smart wallet + * @param params.signer - Local account used for signing transactions + * @param params.ownerIndex - Index of the signer in the wallet's owner list (defaults to 0) + * @returns SmartWallet instance + */ + getWallet(params: { walletAddress: Address signer: LocalAccount ownerIndex?: number - nonce?: bigint - }): Promise { - const { walletAddress, signer, ownerIndex, nonce } = params + }): SmartWallet { + const { walletAddress, signer, ownerIndex } = params return new SmartWallet( [signer.address], signer, @@ -79,7 +87,6 @@ export class SmartWalletProvider { this.paymasterAndBundlerUrl, walletAddress, ownerIndex, - nonce, ) } } From edabe86bbf0353cf859c8658b3b4aeb8e655011f Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 25 Aug 2025 15:17:57 -0700 Subject: [PATCH 13/39] remove baseSepolia from smart wallet --- packages/demo/backend/src/services/wallet.ts | 10 ++-- packages/sdk/src/services/ChainManager.ts | 6 +- packages/sdk/src/wallet/PrivyWallet.ts | 8 +++ packages/sdk/src/wallet/SmartWallet.ts | 58 ++++++++++++++----- .../sdk/src/wallet/providers/smartWallet.ts | 44 ++++++++++++-- 5 files changed, 100 insertions(+), 26 deletions(-) diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index bc933163..4f105907 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -30,10 +30,10 @@ export async function createWallet(): Promise<{ const verbs = getVerbs() const privyWallet = await verbs.wallet.privy!.createWallet() const signer = await privyWallet.signer() - const smartWallet = await verbs.wallet.smartWallet!.createWallet( - [getAddress(privyWallet.address)], + const smartWallet = await verbs.wallet.smartWallet!.createWallet({ + owners: [getAddress(privyWallet.address)], signer, - ) + }) const smartWalletAddress = await smartWallet.getAddress() return { privyAddress: privyWallet.address, @@ -57,7 +57,7 @@ export async function getWallet(userId: string): Promise<{ throw new Error('Wallet not found') } const signer = await privyWallet.signer() - const walletAddress = await verbs.wallet.smartWallet.getSmartWalletAddress({ + const walletAddress = await verbs.wallet.smartWallet.getAddress({ owners: [getAddress(privyWallet.address)], }) const wallet = verbs.wallet.smartWallet.getWallet({ @@ -82,7 +82,7 @@ export async function getAllWallets( throw new Error('Smart wallet not configured') } const walletAddress = - await verbs.wallet.smartWallet.getSmartWalletAddress({ + await verbs.wallet.smartWallet.getAddress({ owners: [getAddress(privyWallet.address)], }) const signer = await privyWallet.signer() diff --git a/packages/sdk/src/services/ChainManager.ts b/packages/sdk/src/services/ChainManager.ts index 0ffe289b..599f859d 100644 --- a/packages/sdk/src/services/ChainManager.ts +++ b/packages/sdk/src/services/ChainManager.ts @@ -1,5 +1,5 @@ import { chainById } from '@eth-optimism/viem/chains' -import { createPublicClient, http, type PublicClient } from 'viem' +import { type Chain, createPublicClient, http, type PublicClient } from 'viem' import type { SUPPORTED_CHAIN_IDS } from '@/constants/supportedChains.js' import type { ChainConfig } from '@/types/chain.js' @@ -36,6 +36,10 @@ export class ChainManager { return chainConfig.rpcUrl } + getChain(chainId: (typeof SUPPORTED_CHAIN_IDS)[number]): Chain { + return chainById[chainId] + } + /** * Get supported chain IDs */ diff --git a/packages/sdk/src/wallet/PrivyWallet.ts b/packages/sdk/src/wallet/PrivyWallet.ts index 3424339a..0b03aa6a 100644 --- a/packages/sdk/src/wallet/PrivyWallet.ts +++ b/packages/sdk/src/wallet/PrivyWallet.ts @@ -32,6 +32,14 @@ export class PrivyWallet { this.address = address } + /** + * Create a LocalAccount signer from this Privy wallet + * @description Converts the Privy wallet into a viem-compatible LocalAccount that can sign + * messages and transactions. The returned account uses Privy's signing infrastructure + * under the hood while providing a standard viem interface. + * @returns Promise resolving to a LocalAccount configured for signing operations + * @throws Error if wallet retrieval fails or signing operations are not supported + */ async signer(): Promise { const privy = this.privyProvider.privy const walletId = this.walletId diff --git a/packages/sdk/src/wallet/SmartWallet.ts b/packages/sdk/src/wallet/SmartWallet.ts index 07923069..d5540506 100644 --- a/packages/sdk/src/wallet/SmartWallet.ts +++ b/packages/sdk/src/wallet/SmartWallet.ts @@ -11,7 +11,7 @@ import { createBundlerClient, toCoinbaseSmartAccount, } from 'viem/account-abstraction' -import { baseSepolia, unichain } from 'viem/chains' +import { unichain } from 'viem/chains' import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' import { smartWalletFactoryAddress } from '@/constants/addresses.js' @@ -34,23 +34,38 @@ import { } from '@/utils/assets.js' /** - * Wallet implementation - * @description Concrete implementation of the Wallet interface + * Smart Wallet Implementation + * @description ERC-4337 compatible smart wallet that uses Coinbase Smart Account (https://github.com/coinbase/smart-wallet/blob/main/src/CoinbaseSmartWallet.sol). + * Supports multi-owner wallets, gasless transactions via paymasters, and cross-chain operations. */ export class SmartWallet { + /** Array of wallet owners (Ethereum addresses or WebAuthn public keys) */ private owners: Array
+ /** Local account used for signing transactions and UserOperations */ private signer: LocalAccount + /** Index of the signer in the owners array (defaults to 0 if not specified) */ private ownerIndex?: number + /** Known deployment address of the wallet (if already deployed) */ private deploymentAddress?: Address + /** Provider for lending market operations */ private lendProvider: LendProvider + /** Manages supported blockchain networks and RPC clients */ private chainManager: ChainManager + /** URL for ERC-4337 bundler and paymaster services */ private bundlerUrl: string + /** Nonce used for deterministic address generation (defaults to 0) */ private nonce?: bigint /** - * Create a new wallet instance - * @param id - Unique wallet identifier - * @param verbs - Verbs instance to access configured providers and chain manager + * Create a Smart Wallet instance + * @param owners - Array of wallet owners (addresses or WebAuthn accounts) + * @param signer - Local account for signing transactions + * @param chainManager - Network management service + * @param lendProvider - Lending operations provider + * @param bundlerUrl - ERC-4337 bundler service URL + * @param deploymentAddress - Known wallet address (if already deployed) + * @param ownerIndex - Index of signer in owners array + * @param nonce - Nonce for address generation */ constructor( owners: Array
, @@ -72,6 +87,12 @@ export class SmartWallet { this.nonce = nonce } + /** + * Get the smart wallet address + * @description Returns the deployment address if known, otherwise calculates the deterministic + * address using CREATE2 based on owners and nonce. + * @returns Promise resolving to the wallet address + */ async getAddress() { if (this.deploymentAddress) return this.deploymentAddress @@ -94,6 +115,12 @@ export class SmartWallet { return smartWalletAddress } + /** + * Create a Coinbase Smart Account instance + * @description Converts this wallet into a viem-compatible smart account for ERC-4337 operations. + * @param chainId - Target blockchain network ID + * @returns Coinbase Smart Account instance configured for the specified chain + */ async getCoinbaseSmartAccount( chainId: SupportedChainId, ): ReturnType { @@ -109,7 +136,8 @@ export class SmartWallet { /** * Get asset balances across all supported chains - * @returns Promise resolving to array of asset balances + * @description Fetches ETH and ERC20 token balances for this wallet across all supported networks. + * @returns Promise resolving to array of token balances with chain breakdown */ async getBalance(): Promise { const address = await this.getAddress() @@ -166,11 +194,13 @@ export class SmartWallet { } /** - * Send a signed transaction - * @description Sends a pre-signed transaction to the network - * @param signedTransaction - Signed transaction to send - * @param publicClient - Viem public client to send the transaction - * @returns Promise resolving to transaction hash + * Send a transaction via ERC-4337 + * @description Executes a transaction using the smart wallet with automatic gas sponsorship. + * The transaction is sent as a UserOperation through the bundler service. + * @param transactionData - Transaction details (to, value, data) + * @param chainId - Target blockchain network ID + * @returns Promise resolving to UserOperation hash + * @throws Error if transaction fails or validation errors occur */ async send( transactionData: TransactionData, @@ -179,14 +209,14 @@ export class SmartWallet { try { const account = await this.getCoinbaseSmartAccount(chainId) const client = createPublicClient({ - chain: baseSepolia, + chain: this.chainManager.getChain(chainId), transport: http(this.bundlerUrl), }) const bundlerClient = createBundlerClient({ account, client, transport: http(this.bundlerUrl), - chain: baseSepolia, + chain: this.chainManager.getChain(chainId), }) const calls = [transactionData] const hash = await bundlerClient.sendUserOperation({ diff --git a/packages/sdk/src/wallet/providers/smartWallet.ts b/packages/sdk/src/wallet/providers/smartWallet.ts index baea1b40..ca57fd21 100644 --- a/packages/sdk/src/wallet/providers/smartWallet.ts +++ b/packages/sdk/src/wallet/providers/smartWallet.ts @@ -8,11 +8,25 @@ import type { ChainManager } from '@/services/ChainManager.js' import type { LendProvider } from '@/types/lend.js' import { SmartWallet } from '@/wallet/SmartWallet.js' +/** + * Smart Wallet Provider + * @description Factory for creating and managing Smart Wallet instances. + * Handles wallet address prediction, creation, and retrieval using ERC-4337 account abstraction. + */ export class SmartWalletProvider { + /** Manages supported blockchain networks */ private chainManager: ChainManager + /** URL for ERC-4337 bundler and paymaster services */ private paymasterAndBundlerUrl: string + /** Provider for lending market operations */ private lendProvider: LendProvider + /** + * Initialize the Smart Wallet Provider + * @param chainManager - Manages supported blockchain networks + * @param paymasterAndBundlerUrl - URL for ERC-4337 bundler and paymaster services + * @param lendProvider - Provider for lending market operations + */ constructor( chainManager: ChainManager, paymasterAndBundlerUrl: string, @@ -23,11 +37,21 @@ export class SmartWalletProvider { this.lendProvider = lendProvider } - async createWallet( - owners: Array
, - signer: LocalAccount, - nonce?: bigint, - ): Promise { + /** + * Create a new smart wallet instance + * @description Creates a new smart wallet that will be deployed on first transaction. + * The wallet address is deterministically calculated from owners and nonce. + * @param owners - Array of wallet owners (addresses or WebAuthn public keys) + * @param signer - Local account used for signing transactions + * @param nonce - Optional nonce for address generation (defaults to 0) + * @returns Promise resolving to a new SmartWallet instance + */ + async createWallet(params: { + owners: Array
+ signer: LocalAccount + nonce?: bigint + }): Promise { + const { owners, signer, nonce } = params return new SmartWallet( owners, signer, @@ -40,7 +64,15 @@ export class SmartWalletProvider { ) } - async getSmartWalletAddress(params: { + /** + * Get the predicted smart wallet address + * @description Calculates the deterministic address where a smart wallet would be deployed + * given the specified owners and nonce. Uses CREATE2 for address prediction. + * @param params.owners - Array of wallet owners (addresses or WebAuthn public keys) + * @param params.nonce - Nonce for address generation (defaults to 0) + * @returns Promise resolving to the predicted wallet address + */ + async getAddress(params: { owners: Array
nonce?: bigint }) { From 11da9676abe5f598ada591e4ded03862768cf6eb Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 25 Aug 2025 15:26:38 -0700 Subject: [PATCH 14/39] move bundler url into rpc config --- packages/demo/backend/src/config/verbs.ts | 5 ++- packages/demo/backend/src/services/wallet.ts | 7 ++-- packages/sdk/src/services/ChainManager.ts | 32 +++++++++++++++++++ packages/sdk/src/types/chain.ts | 2 ++ packages/sdk/src/types/verbs.ts | 13 ++------ packages/sdk/src/verbs.ts | 8 ++--- packages/sdk/src/wallet/SmartWallet.ts | 28 ++-------------- packages/sdk/src/wallet/WalletNamespace.ts | 12 ++----- .../sdk/src/wallet/providers/smartWallet.ts | 11 +------ 9 files changed, 49 insertions(+), 69 deletions(-) diff --git a/packages/demo/backend/src/config/verbs.ts b/packages/demo/backend/src/config/verbs.ts index 23f8e078..11626c11 100644 --- a/packages/demo/backend/src/config/verbs.ts +++ b/packages/demo/backend/src/config/verbs.ts @@ -27,11 +27,10 @@ export function createVerbsConfig(): VerbsConfig { { chainId: baseSepolia.id, rpcUrl: env.BASE_SEPOLIA_RPC_URL || baseSepolia.rpcUrls.default.http[0], + bundlerUrl: env.BUNDLER_URL, }, ], - smartWalletConfig: { - bundlerUrl: env.BUNDLER_URL, - }, + enableSmartWallets: true, } } diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index 4f105907..13e8a97e 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -81,10 +81,9 @@ export async function getAllWallets( if (!verbs.wallet.smartWallet) { throw new Error('Smart wallet not configured') } - const walletAddress = - await verbs.wallet.smartWallet.getAddress({ - owners: [getAddress(privyWallet.address)], - }) + const walletAddress = await verbs.wallet.smartWallet.getAddress({ + owners: [getAddress(privyWallet.address)], + }) const signer = await privyWallet.signer() return { privyWallet, diff --git a/packages/sdk/src/services/ChainManager.ts b/packages/sdk/src/services/ChainManager.ts index 599f859d..0ac0a7d8 100644 --- a/packages/sdk/src/services/ChainManager.ts +++ b/packages/sdk/src/services/ChainManager.ts @@ -1,5 +1,7 @@ import { chainById } from '@eth-optimism/viem/chains' import { type Chain, createPublicClient, http, type PublicClient } from 'viem' +import type { BundlerClient, SmartAccount } from 'viem/account-abstraction' +import { createBundlerClient } from 'viem/account-abstraction' import type { SUPPORTED_CHAIN_IDS } from '@/constants/supportedChains.js' import type { ChainConfig } from '@/types/chain.js' @@ -28,6 +30,26 @@ export class ChainManager { return client } + getBundlerClient( + chainId: (typeof SUPPORTED_CHAIN_IDS)[number], + account: SmartAccount, + ): BundlerClient { + const bundlerUrl = this.getBundlerUrl(chainId) + if (!bundlerUrl) { + throw new Error(`No bundler URL configured for chain ID: ${chainId}`) + } + const client = createPublicClient({ + chain: this.getChain(chainId), + transport: http(bundlerUrl), + }) + return createBundlerClient({ + account, + client, + transport: http(bundlerUrl), + chain: this.getChain(chainId), + }) + } + getRpcUrl(chainId: (typeof SUPPORTED_CHAIN_IDS)[number]): string { const chainConfig = this.chainConfigs.find((c) => c.chainId === chainId) if (!chainConfig) { @@ -36,6 +58,16 @@ export class ChainManager { return chainConfig.rpcUrl } + getBundlerUrl( + chainId: (typeof SUPPORTED_CHAIN_IDS)[number], + ): string | undefined { + const chainConfig = this.chainConfigs.find((c) => c.chainId === chainId) + if (!chainConfig) { + throw new Error(`No chain config found for chain ID: ${chainId}`) + } + return chainConfig.bundlerUrl + } + getChain(chainId: (typeof SUPPORTED_CHAIN_IDS)[number]): Chain { return chainById[chainId] } diff --git a/packages/sdk/src/types/chain.ts b/packages/sdk/src/types/chain.ts index a87ba909..6f80bd45 100644 --- a/packages/sdk/src/types/chain.ts +++ b/packages/sdk/src/types/chain.ts @@ -9,4 +9,6 @@ export interface ChainConfig { chainId: (typeof SUPPORTED_CHAIN_IDS)[number] /** RPC URL for the chain */ rpcUrl: string + /** Bundler URL for the chain */ + bundlerUrl?: string } diff --git a/packages/sdk/src/types/verbs.ts b/packages/sdk/src/types/verbs.ts index 6a87ccce..f11f81b7 100644 --- a/packages/sdk/src/types/verbs.ts +++ b/packages/sdk/src/types/verbs.ts @@ -52,8 +52,8 @@ export interface VerbsInterface { export interface VerbsConfig { /** Wallet provider configuration */ privyConfig?: PrivyWalletConfig - /** Smart wallet provider configuration */ - smartWalletConfig?: SmartWalletConfig + /** Enable smart wallets */ + enableSmartWallets?: boolean /** Lending provider configuration (optional) */ lend?: LendConfig /** Chains to use for the SDK */ @@ -78,12 +78,3 @@ export interface PrivyWalletConfig { /** Privy app secret */ appSecret: string } - -/** - * Smart wallet provider configuration - * @description Configuration for smart wallet provider - */ -export interface SmartWalletConfig { - /** Bundler URL for the smart wallet */ - bundlerUrl: string -} diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index 8b47b059..8ba78a29 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -76,12 +76,8 @@ export function initVerbs(config: VerbsConfig) { verbs.chainManager, ) } - if (config.smartWalletConfig) { - verbs.wallet.withSmartWallet( - verbs.chainManager, - config.smartWalletConfig.bundlerUrl, - verbs.lend, - ) + if (config.enableSmartWallets) { + verbs.wallet.withSmartWallet(verbs.chainManager, verbs.lend) } return verbs diff --git a/packages/sdk/src/wallet/SmartWallet.ts b/packages/sdk/src/wallet/SmartWallet.ts index d5540506..517fce10 100644 --- a/packages/sdk/src/wallet/SmartWallet.ts +++ b/packages/sdk/src/wallet/SmartWallet.ts @@ -1,16 +1,7 @@ import type { Address, Hash, LocalAccount } from 'viem' -import { - createPublicClient, - encodeFunctionData, - erc20Abi, - http, - pad, -} from 'viem' +import { encodeFunctionData, erc20Abi, pad } from 'viem' import type { WebAuthnAccount } from 'viem/account-abstraction' -import { - createBundlerClient, - toCoinbaseSmartAccount, -} from 'viem/account-abstraction' +import { toCoinbaseSmartAccount } from 'viem/account-abstraction' import { unichain } from 'viem/chains' import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' @@ -51,8 +42,6 @@ export class SmartWallet { private lendProvider: LendProvider /** Manages supported blockchain networks and RPC clients */ private chainManager: ChainManager - /** URL for ERC-4337 bundler and paymaster services */ - private bundlerUrl: string /** Nonce used for deterministic address generation (defaults to 0) */ private nonce?: bigint @@ -72,7 +61,6 @@ export class SmartWallet { signer: LocalAccount, chainManager: ChainManager, lendProvider: LendProvider, - bundlerUrl: string, deploymentAddress?: Address, ownerIndex?: number, nonce?: bigint, @@ -83,7 +71,6 @@ export class SmartWallet { this.deploymentAddress = deploymentAddress this.chainManager = chainManager this.lendProvider = lendProvider - this.bundlerUrl = bundlerUrl this.nonce = nonce } @@ -208,16 +195,7 @@ export class SmartWallet { ): Promise { try { const account = await this.getCoinbaseSmartAccount(chainId) - const client = createPublicClient({ - chain: this.chainManager.getChain(chainId), - transport: http(this.bundlerUrl), - }) - const bundlerClient = createBundlerClient({ - account, - client, - transport: http(this.bundlerUrl), - chain: this.chainManager.getChain(chainId), - }) + const bundlerClient = this.chainManager.getBundlerClient(chainId, account) const calls = [transactionData] const hash = await bundlerClient.sendUserOperation({ account, diff --git a/packages/sdk/src/wallet/WalletNamespace.ts b/packages/sdk/src/wallet/WalletNamespace.ts index 708e8f03..3a1d3fc2 100644 --- a/packages/sdk/src/wallet/WalletNamespace.ts +++ b/packages/sdk/src/wallet/WalletNamespace.ts @@ -13,16 +13,8 @@ export class WalletNamespace { return this } - withSmartWallet( - chainManager: ChainManager, - bundlerUrl: string, - lendProvider: LendProvider, - ) { - this.smartWallet = new SmartWalletProvider( - chainManager, - bundlerUrl, - lendProvider, - ) + withSmartWallet(chainManager: ChainManager, lendProvider: LendProvider) { + this.smartWallet = new SmartWalletProvider(chainManager, lendProvider) return this } } diff --git a/packages/sdk/src/wallet/providers/smartWallet.ts b/packages/sdk/src/wallet/providers/smartWallet.ts index ca57fd21..33b117f5 100644 --- a/packages/sdk/src/wallet/providers/smartWallet.ts +++ b/packages/sdk/src/wallet/providers/smartWallet.ts @@ -16,8 +16,6 @@ import { SmartWallet } from '@/wallet/SmartWallet.js' export class SmartWalletProvider { /** Manages supported blockchain networks */ private chainManager: ChainManager - /** URL for ERC-4337 bundler and paymaster services */ - private paymasterAndBundlerUrl: string /** Provider for lending market operations */ private lendProvider: LendProvider @@ -27,13 +25,8 @@ export class SmartWalletProvider { * @param paymasterAndBundlerUrl - URL for ERC-4337 bundler and paymaster services * @param lendProvider - Provider for lending market operations */ - constructor( - chainManager: ChainManager, - paymasterAndBundlerUrl: string, - lendProvider: LendProvider, - ) { + constructor(chainManager: ChainManager, lendProvider: LendProvider) { this.chainManager = chainManager - this.paymasterAndBundlerUrl = paymasterAndBundlerUrl this.lendProvider = lendProvider } @@ -57,7 +50,6 @@ export class SmartWalletProvider { signer, this.chainManager, this.lendProvider, - this.paymasterAndBundlerUrl, undefined, undefined, nonce, @@ -116,7 +108,6 @@ export class SmartWalletProvider { signer, this.chainManager, this.lendProvider, - this.paymasterAndBundlerUrl, walletAddress, ownerIndex, ) From 7e0eb69e14d275a7abecb1d37f88c37c1b87fea4 Mon Sep 17 00:00:00 2001 From: tre Date: Mon, 25 Aug 2025 15:29:33 -0700 Subject: [PATCH 15/39] update env var name --- packages/demo/backend/src/config/env.ts | 2 +- packages/demo/backend/src/config/verbs.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/demo/backend/src/config/env.ts b/packages/demo/backend/src/config/env.ts index e58b7305..016db175 100644 --- a/packages/demo/backend/src/config/env.ts +++ b/packages/demo/backend/src/config/env.ts @@ -54,5 +54,5 @@ export const env = cleanEnv(process.env, { FAUCET_ADDRESS: str({ default: getFaucetAddressDefault(), }), - BUNDLER_URL: str(), + BASE_SEPOLIA_BUNDER_URL: str(), }) diff --git a/packages/demo/backend/src/config/verbs.ts b/packages/demo/backend/src/config/verbs.ts index 11626c11..661a5531 100644 --- a/packages/demo/backend/src/config/verbs.ts +++ b/packages/demo/backend/src/config/verbs.ts @@ -27,7 +27,7 @@ export function createVerbsConfig(): VerbsConfig { { chainId: baseSepolia.id, rpcUrl: env.BASE_SEPOLIA_RPC_URL || baseSepolia.rpcUrls.default.http[0], - bundlerUrl: env.BUNDLER_URL, + bundlerUrl: env.BASE_SEPOLIA_BUNDER_URL, }, ], enableSmartWallets: true, From 74c5204c9bf70c019a108dcf01fb860fbef7a79d Mon Sep 17 00:00:00 2001 From: tre Date: Tue, 26 Aug 2025 17:00:29 -0700 Subject: [PATCH 16/39] refactor to more extensible classes and providers --- packages/demo/backend/src/app.ts | 1 + packages/demo/backend/src/config/verbs.ts | 26 ++-- .../demo/backend/src/controllers/wallet.ts | 22 ++- packages/demo/backend/src/services/wallet.ts | 81 ++++------ packages/demo/backend/src/types/index.ts | 1 + .../backend}/src/types/service.ts | 2 +- packages/demo/frontend/package.json | 1 + packages/demo/frontend/src/api/verbsApi.ts | 2 +- .../demo/frontend/src/components/Terminal.tsx | 2 +- packages/sdk/src/index.ts | 19 +-- packages/sdk/src/types/index.ts | 1 - packages/sdk/src/types/verbs.ts | 103 ++++++------- packages/sdk/src/types/wallet.ts | 141 ++---------------- packages/sdk/src/verbs.test.ts | 85 ++++++++--- packages/sdk/src/verbs.ts | 64 +++++--- packages/sdk/src/wallet/PrivyWallet.ts | 67 +-------- packages/sdk/src/wallet/SmartWallet.ts | 20 ++- packages/sdk/src/wallet/WalletNamespace.ts | 45 ++++-- packages/sdk/src/wallet/WalletProvider.ts | 67 +++++++++ .../sdk/src/wallet/base/EmbeddedWallet.ts | 11 ++ packages/sdk/src/wallet/base/SmartWallet.ts | 32 ++++ ...allet.ts => DefaultSmartWalletProvider.ts} | 16 +- .../providers/base/EmbeddedWalletProvider.ts | 10 ++ .../providers/base/SmartWalletProvider.ts | 27 ++++ packages/sdk/src/wallet/providers/privy.ts | 35 +++-- pnpm-lock.yaml | 3 + 26 files changed, 472 insertions(+), 412 deletions(-) create mode 100644 packages/demo/backend/src/types/index.ts rename packages/{sdk => demo/backend}/src/types/service.ts (97%) create mode 100644 packages/sdk/src/wallet/WalletProvider.ts create mode 100644 packages/sdk/src/wallet/base/EmbeddedWallet.ts create mode 100644 packages/sdk/src/wallet/base/SmartWallet.ts rename packages/sdk/src/wallet/providers/{smartWallet.ts => DefaultSmartWalletProvider.ts} (91%) create mode 100644 packages/sdk/src/wallet/providers/base/EmbeddedWalletProvider.ts create mode 100644 packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts diff --git a/packages/demo/backend/src/app.ts b/packages/demo/backend/src/app.ts index 9d5d9a5b..e30566a4 100644 --- a/packages/demo/backend/src/app.ts +++ b/packages/demo/backend/src/app.ts @@ -84,4 +84,5 @@ class VerbsApp extends App { } } +export * from '@/types/index.js' export { VerbsApp } diff --git a/packages/demo/backend/src/config/verbs.ts b/packages/demo/backend/src/config/verbs.ts index 661a5531..c52aa29f 100644 --- a/packages/demo/backend/src/config/verbs.ts +++ b/packages/demo/backend/src/config/verbs.ts @@ -1,8 +1,4 @@ -import { - initVerbs, - type Verbs, - type VerbsConfig, -} from '@eth-optimism/verbs-sdk' +import { Verbs, type VerbsConfig } from '@eth-optimism/verbs-sdk' import { baseSepolia, unichain } from 'viem/chains' import { env } from './env.js' @@ -11,10 +7,19 @@ let verbsInstance: Verbs export function createVerbsConfig(): VerbsConfig { return { - privyConfig: { - type: 'privy', - appId: env.PRIVY_APP_ID, - appSecret: env.PRIVY_APP_SECRET, + wallet: { + embeddedWalletConfig: { + provider: { + type: 'privy', + appId: env.PRIVY_APP_ID, + appSecret: env.PRIVY_APP_SECRET, + }, + }, + smartWalletConfig: { + provider: { + type: 'default', + }, + }, }, lend: { type: 'morpho', @@ -30,13 +35,12 @@ export function createVerbsConfig(): VerbsConfig { bundlerUrl: env.BASE_SEPOLIA_BUNDER_URL, }, ], - enableSmartWallets: true, } } export function initializeVerbs(config?: VerbsConfig): void { const verbsConfig = config || createVerbsConfig() - verbsInstance = initVerbs(verbsConfig) + verbsInstance = new Verbs(verbsConfig) } export function getVerbs() { diff --git a/packages/demo/backend/src/controllers/wallet.ts b/packages/demo/backend/src/controllers/wallet.ts index 0f222b3c..0efe3af3 100644 --- a/packages/demo/backend/src/controllers/wallet.ts +++ b/packages/demo/backend/src/controllers/wallet.ts @@ -1,11 +1,12 @@ +import type { Context } from 'hono' +import type { Address } from 'viem' +import { z } from 'zod' + import type { CreateWalletResponse, GetAllWalletsResponse, GetWalletResponse, -} from '@eth-optimism/verbs-sdk' -import type { Context } from 'hono' -import type { Address } from 'viem' -import { z } from 'zod' +} from '@/types/service.js' import { validateRequest } from '../helpers/validation.js' import * as walletService from '../services/wallet.js' @@ -107,6 +108,7 @@ export class WalletController { userId, } satisfies GetWalletResponse) } catch (error) { + console.error(error) return c.json( { error: 'Failed to get wallet', @@ -129,15 +131,19 @@ export class WalletController { query: { limit, cursor }, } = validation.data const wallets = await walletService.getAllWallets({ limit, cursor }) + const walletsData = await Promise.all( + wallets.map(async ({ wallet, id }) => ({ + address: await wallet.getAddress(), + id, + })), + ) return c.json({ - wallets: wallets.map(({ privyWallet }) => ({ - address: privyWallet.address as Address, - id: privyWallet.walletId, - })), + wallets: walletsData, count: wallets.length, } satisfies GetAllWalletsResponse) } catch (error) { + console.error(error) return c.json( { error: 'Failed to get wallets', diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index 13e8a97e..4192d1ed 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -1,7 +1,8 @@ import type { - GetAllWalletsOptions, - PrivyWallet, + PrivyEmbeddedWalletProvider, + PrivyProviderGetAllWalletsOptions as GetAllWalletsOptions, SmartWallet, + SmartWalletProvider, TokenBalance, TransactionData, } from '@eth-optimism/verbs-sdk' @@ -12,7 +13,6 @@ import { createWalletClient, formatEther, formatUnits, - getAddress, http, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' @@ -28,69 +28,49 @@ export async function createWallet(): Promise<{ smartWalletAddress: string }> { const verbs = getVerbs() - const privyWallet = await verbs.wallet.privy!.createWallet() - const signer = await privyWallet.signer() - const smartWallet = await verbs.wallet.smartWallet!.createWallet({ - owners: [getAddress(privyWallet.address)], - signer, - }) - const smartWalletAddress = await smartWallet.getAddress() + const wallet = await verbs.wallet.createWallet() + const smartWalletAddress = await wallet.getAddress() return { - privyAddress: privyWallet.address, + privyAddress: wallet.signer.address, smartWalletAddress, } } export async function getWallet(userId: string): Promise<{ - privyWallet: PrivyWallet wallet: SmartWallet }> { const verbs = getVerbs() - if (!verbs.wallet.privy) { - throw new Error('Privy wallet not configured') - } - if (!verbs.wallet.smartWallet) { - throw new Error('Smart wallet not configured') - } - const privyWallet = await verbs.wallet.privy.getWallet(userId) - if (!privyWallet) { - throw new Error('Wallet not found') - } - const signer = await privyWallet.signer() - const walletAddress = await verbs.wallet.smartWallet.getAddress({ - owners: [getAddress(privyWallet.address)], - }) - const wallet = verbs.wallet.smartWallet.getWallet({ - walletAddress, - signer, + const wallet = await verbs.wallet.getWallet({ + walletId: userId, }) - return { privyWallet, wallet } + return { wallet } } export async function getAllWallets( options?: GetAllWalletsOptions, -): Promise> { +): Promise> { try { const verbs = getVerbs() - if (!verbs.wallet.privy) { - throw new Error('Privy wallet not configured') - } - const privyWallets = await verbs.wallet.privy.getAllWallets(options) + const privyWallets = await ( + verbs.wallet.embeddedWalletProvider as PrivyEmbeddedWalletProvider + ).getAllWallets(options) return Promise.all( privyWallets.map(async (privyWallet) => { - if (!verbs.wallet.smartWallet) { - throw new Error('Smart wallet not configured') - } - const walletAddress = await verbs.wallet.smartWallet.getAddress({ - owners: [getAddress(privyWallet.address)], - }) + const walletAddress = + await verbs.wallet.smartWalletProvider.getWalletAddress({ + owners: [privyWallet.address], + }) const signer = await privyWallet.signer() + const wallet = await ( + verbs.wallet.smartWalletProvider as SmartWalletProvider + ).getWallet({ + walletAddress, + signer, + ownerIndex: 0, + }) return { - privyWallet, - wallet: verbs.wallet.smartWallet.getWallet({ - walletAddress, - signer, - }), + wallet, + id: privyWallet.walletId, } }), ) @@ -101,9 +81,6 @@ export async function getAllWallets( export async function getBalance(userId: string): Promise { const { wallet } = await getWallet(userId) - if (!wallet) { - throw new Error('Wallet not found') - } // Get regular token balances const tokenBalances = await wallet.getBalance().catch((error) => { @@ -175,7 +152,7 @@ export async function fundWallet( // TODO: do this a better way const isLocalSupersim = env.LOCAL_DEV - const { wallet, privyWallet } = await getWallet(userId) + const { wallet } = await getWallet(userId) if (!wallet) { throw new Error('Wallet not found') } @@ -220,7 +197,7 @@ Funding is only available in local development with supersim`) address: env.FAUCET_ADDRESS as Address, abi: faucetAbi, functionName: 'dripETH', - args: [privyWallet.address as `0x${string}`, amount], + args: [wallet.signer.address as `0x${string}`, amount], }) } else { amount = 1000000000n // 1000 USDC @@ -248,7 +225,7 @@ Funding is only available in local development with supersim`) success: true, tokenType, to: walletAddress, - privyAddress: privyWallet.address, + privyAddress: wallet.signer.address, amount: formattedAmount, } } diff --git a/packages/demo/backend/src/types/index.ts b/packages/demo/backend/src/types/index.ts new file mode 100644 index 00000000..9b02e887 --- /dev/null +++ b/packages/demo/backend/src/types/index.ts @@ -0,0 +1 @@ +export * from '@/types/service.js' diff --git a/packages/sdk/src/types/service.ts b/packages/demo/backend/src/types/service.ts similarity index 97% rename from packages/sdk/src/types/service.ts rename to packages/demo/backend/src/types/service.ts index ad8ad907..64a9f171 100644 --- a/packages/sdk/src/types/service.ts +++ b/packages/demo/backend/src/types/service.ts @@ -11,7 +11,7 @@ import type { Address } from 'viem' export interface WalletData { /** Wallet address */ address: Address - /** ID of the wallet */ + /** Wallet ID */ id: string } diff --git a/packages/demo/frontend/package.json b/packages/demo/frontend/package.json index 8ae0870a..6a96701a 100644 --- a/packages/demo/frontend/package.json +++ b/packages/demo/frontend/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@eth-optimism/verbs-sdk": "workspace:*", + "@eth-optimism/verbs-service": "workspace:*", "react": "^18", "react-dom": "^18", "react-router": "7", diff --git a/packages/demo/frontend/src/api/verbsApi.ts b/packages/demo/frontend/src/api/verbsApi.ts index 6a0520e2..6c14228d 100644 --- a/packages/demo/frontend/src/api/verbsApi.ts +++ b/packages/demo/frontend/src/api/verbsApi.ts @@ -1,7 +1,7 @@ import type { CreateWalletResponse, GetAllWalletsResponse, -} from '@eth-optimism/verbs-sdk' +} from '@eth-optimism/verbs-service' import { env } from '../envVars' class VerbsApiError extends Error { diff --git a/packages/demo/frontend/src/components/Terminal.tsx b/packages/demo/frontend/src/components/Terminal.tsx index 4b3352ca..6f4d3312 100644 --- a/packages/demo/frontend/src/components/Terminal.tsx +++ b/packages/demo/frontend/src/components/Terminal.tsx @@ -3,7 +3,7 @@ import type { CreateWalletResponse, GetAllWalletsResponse, WalletData, -} from '@eth-optimism/verbs-sdk' +} from '@eth-optimism/verbs-service' import NavBar from './NavBar' import { verbsApi } from '../api/verbsApi' import type { Address } from 'viem' diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index eeec71d2..0e6f3099 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -3,11 +3,6 @@ export { LendProvider, LendProviderMorpho } from './lend/index.js' export { getTokenAddress, SUPPORTED_TOKENS } from './supported/tokens.js' export type { ApyBreakdown, - CreateWalletResponse, - ErrorResponse, - GetAllWalletsOptions, - GetAllWalletsResponse, - GetWalletResponse, LendConfig, LendMarket, LendMarketInfo, @@ -15,16 +10,16 @@ export type { LendTransaction, LendVaultInfo, MorphoLendConfig, - PrivyWalletConfig, TokenBalance, TransactionData, VerbsConfig, - VerbsInterface, WalletConfig, - WalletData, - Wallet as WalletInterface, - WalletProvider, } from './types/index.js' -export { initVerbs, Verbs } from './verbs.js' +export { Verbs } from './verbs.js' +export { SmartWallet } from './wallet/base/SmartWallet.js' export { PrivyWallet } from './wallet/PrivyWallet.js' -export { SmartWallet } from './wallet/SmartWallet.js' +export { SmartWalletProvider } from './wallet/providers/base/SmartWalletProvider.js' +export { + PrivyEmbeddedWalletProvider, + type PrivyProviderGetAllWalletsOptions, +} from './wallet/providers/privy.js' diff --git a/packages/sdk/src/types/index.ts b/packages/sdk/src/types/index.ts index f35c3448..8da53b57 100644 --- a/packages/sdk/src/types/index.ts +++ b/packages/sdk/src/types/index.ts @@ -1,6 +1,5 @@ export * from './chain.js' export * from './lend.js' -export * from './service.js' export * from './token.js' export * from './verbs.js' export * from './wallet.js' diff --git a/packages/sdk/src/types/verbs.ts b/packages/sdk/src/types/verbs.ts index f11f81b7..d683cfc7 100644 --- a/packages/sdk/src/types/verbs.ts +++ b/packages/sdk/src/types/verbs.ts @@ -1,59 +1,14 @@ -import type { Address } from 'viem' - -import type { ChainManager } from '@/services/ChainManager.js' import type { ChainConfig } from '@/types/chain.js' -import type { LendConfig, LendProvider } from './lend.js' -import type { Wallet } from './wallet.js' - -/** - * Core Verbs SDK interface - * @description Main interface for interacting with the Verbs SDK - */ -export interface VerbsInterface { - /** - * Get the lend provider instance - * @returns LendProvider instance if configured - */ - readonly lend: LendProvider - /** - * Get the chain manager instance - * @returns ChainManager instance for multi-chain operations - */ - readonly chainManager: ChainManager - /** - * Create a new wallet - * @param ownerAddresses - User identifier for the wallet - * @returns Promise resolving to new wallet instance - */ - createWallet( - ownerAddresses: Address[], - nonce?: bigint, - ): Promise> - /** - * Get all wallets - * @param options - Optional parameters for filtering and pagination - * @returns Promise resolving to array of wallets - */ - // getAllWallets(options?: GetAllWalletsOptions): Promise - /** - * Get the smart wallet address for an owner address - * @param ownerAddress - Owner address - * @param chainId - Chain ID - * @returns Promise resolving to smart wallet address - */ - getWallet(ownerAddresses: Address[], nonce?: bigint): Promise -} +import type { LendConfig } from './lend.js' /** * Verbs SDK configuration * @description Configuration object for initializing the Verbs SDK */ export interface VerbsConfig { - /** Wallet provider configuration */ - privyConfig?: PrivyWalletConfig - /** Enable smart wallets */ - enableSmartWallets?: boolean + /** Wallet configuration */ + wallet: WalletConfig /** Lending provider configuration (optional) */ lend?: LendConfig /** Chains to use for the SDK */ @@ -61,17 +16,57 @@ export interface VerbsConfig { } /** - * Wallet provider configuration + * Wallet configuration * @description Configuration for wallet providers */ -export type WalletConfig = PrivyWalletConfig +export type WalletConfig = { + /** Embedded wallet configuration */ + embeddedWalletConfig: EmbeddedWalletConfig + /** Smart wallet configuration for ERC-4337 infrastructure */ + smartWalletConfig: SmartWalletConfig +} + +/** + * Embedded wallet configuration + * @description Configuration for embedded wallets / signers + */ +export interface EmbeddedWalletConfig { + /** Wallet provider for account creation, management, and signing */ + provider: EmbeddedWalletProviderConfig +} + +/** + * Smart Wallet configuration + * @description Configuration for ERC-4337 smart wallets. + */ +export interface SmartWalletConfig { + /** Wallet provider for smart wallet management */ + provider: SmartWalletProvider +} + +/** + * Smart wallet provider configurations + * @description Union type supporting multiple wallet provider implementations + */ +export type SmartWalletProvider = DefaultSmartWalletProvider /** - * Privy wallet provider configuration - * @description Configuration specific to Privy wallet provider + * Default smart wallet provider configuration + * @description Built-in provider smart wallet provider. */ -export interface PrivyWalletConfig { - /** Wallet provider type */ +export interface DefaultSmartWalletProvider { + type: 'default' +} + +/** + * Embedded wallet provider configurations + * @description Union type supporting multiple embedded wallet providers + */ +export type EmbeddedWalletProviderConfig = PrivyEmbeddedWalletProviderConfig + +/** Privy embedded wallet provider configuration */ +export interface PrivyEmbeddedWalletProviderConfig { + /** Embedded wallet provider type */ type: 'privy' /** Privy app ID */ appId: string diff --git a/packages/sdk/src/types/wallet.ts b/packages/sdk/src/types/wallet.ts index f8ea9eb8..175cd4d6 100644 --- a/packages/sdk/src/types/wallet.ts +++ b/packages/sdk/src/types/wallet.ts @@ -1,135 +1,14 @@ -import type { Address, Hash, Hex, Quantity } from 'viem' - -import type { SupportedChainId } from '@/constants/supportedChains.js' -import type { - LendOptions, - LendTransaction, - TransactionData, -} from '@/types/lend.js' -import type { TokenBalance } from '@/types/token.js' -import type { AssetIdentifier } from '@/utils/assets.js' - -/** - * Wallet provider interface - * @description Interface for wallet provider implementations - */ -export interface WalletProvider { - /** - * Create a new wallet - * @param userId - User identifier for the wallet - * @returns Promise resolving to new wallet instance - */ - createWallet(userId: string): Promise - /** - * Get wallet by user ID - * @param userId - User identifier - * @returns Promise resolving to wallet or null if not found - */ - getWallet(userId: string): Promise - /** - * Get all wallets - * @param options - Optional parameters for filtering and pagination - * @returns Promise resolving to array of wallets - */ - getAllWallets(options?: GetAllWalletsOptions): Promise - /** - * Sign and send a transaction - * @param walletId - Wallet ID to use for signing - * @param transactionData - Transaction data to sign and send - * @returns Promise resolving to transaction hash - */ -} - -/** - * Wallet interface - * @description Core wallet interface with blockchain properties and verbs - */ -export interface Wallet { - /** Wallet address */ - address: Address - /** Wallet owner addresses */ - ownerAddresses: Address[] - /** - * Get asset balances aggregated across all supported chains - * @returns Promise resolving to array of asset balances - */ - getBalance(): Promise - /** - * Lend assets to a lending market - * @param amount - Human-readable amount to lend (e.g. 1.5) - * @param asset - Asset symbol (e.g. 'usdc') or token address - * @param marketId - Optional specific market ID or vault name - * @param options - Optional lending configuration - * @returns Promise resolving to lending transaction details - */ - lend( - amount: number, - asset: AssetIdentifier, - marketId?: string, - options?: LendOptions, - ): Promise - /** - * Send a signed transaction - * @param signedTransaction - Signed transaction to send - * @param publicClient - Viem public client to send the transaction - * @returns Promise resolving to transaction hash - */ - send(signedTransaction: string, publicClient: any): Promise - /** - * Send tokens to another address - * @param amount - Human-readable amount to send (e.g. 1.5) - * @param asset - Asset symbol (e.g. 'usdc', 'eth') or token address - * @param recipientAddress - Address to send to - * @returns Promise resolving to transaction data - */ - sendTokens( - amount: number, - asset: AssetIdentifier, - recipientAddress: Address, - ): Promise - execute(transactionData: TransactionData): Hash - getTxParams( - transactionData: TransactionData, - chainId: SupportedChainId, - ownerIndex?: number, - ): Promise<{ - /** The address the transaction is sent from. Must be hexadecimal formatted. */ - from?: Hex - /** Destination address of the transaction. */ - to?: Hex - /** The nonce to be used for the transaction (hexadecimal or number). */ - nonce?: Quantity - /** (optional) The chain ID of network your transaction will be sent on. */ - chainId?: Quantity - /** (optional) Data to send to the receiving address, especially when calling smart contracts. Must be hexadecimal formatted. */ - data?: Hex - /** (optional) The value (in wei) be sent with the transaction (hexadecimal or number). */ - value?: Quantity - /** (optional) The EIP-2718 transction type (e.g. `2` for EIP-1559 transactions). */ - type?: 0 | 1 | 2 - /** (optional) The max units of gas that can be used by this transaction (hexadecimal or number). */ - gasLimit?: Quantity - /** (optional) The price (in wei) per unit of gas for this transaction (hexadecimal or number), for use in non EIP-1559 transactions (type 0 or 1). */ - gasPrice?: Quantity - /** (optional) The maxFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ - maxFeePerGas?: Quantity - /** (optional) The maxPriorityFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ - maxPriorityFeePerGas?: Quantity - }> - estimateGas( - transactionData: TransactionData, - chainId: SupportedChainId, - ownerIndex?: number, - ): Promise -} +import type { Address } from 'viem' +import type { WebAuthnAccount } from 'viem/account-abstraction' /** - * Options for getting all wallets - * @description Parameters for filtering and paginating wallet results + * Options for getting a wallet + * @description Parameters for getting a wallet */ -export interface GetAllWalletsOptions { - /** Maximum number of wallets to return */ - limit?: number - /** Cursor for pagination */ - cursor?: string +export type GetWalletOptions = { + walletId: string + owners?: Array
+ signerOwnerIndex?: number + walletAddress?: Address + nonce?: bigint } diff --git a/packages/sdk/src/verbs.test.ts b/packages/sdk/src/verbs.test.ts index af5b6cb5..acfa95f1 100644 --- a/packages/sdk/src/verbs.test.ts +++ b/packages/sdk/src/verbs.test.ts @@ -16,10 +16,19 @@ describe('Verbs SDK - System Tests', () => { type: 'morpho', defaultSlippage: 50, }, - privyConfig: { - type: 'privy', - appId: 'test-app-id', - appSecret: 'test-app-secret', + wallet: { + embeddedWalletConfig: { + provider: { + type: 'privy', + appId: 'test-app-id', + appSecret: 'test-app-secret', + }, + }, + smartWalletConfig: { + provider: { + type: 'default', + }, + }, }, }) @@ -76,10 +85,19 @@ describe('Verbs SDK - System Tests', () => { type: 'morpho', defaultSlippage: 50, }, - privyConfig: { - type: 'privy', - appId: 'test-app-id', - appSecret: 'test-app-secret', + wallet: { + embeddedWalletConfig: { + provider: { + type: 'privy', + appId: 'test-app-id', + appSecret: 'test-app-secret', + }, + }, + smartWalletConfig: { + provider: { + type: 'default', + }, + }, }, }) @@ -107,10 +125,19 @@ describe('Verbs SDK - System Tests', () => { type: 'morpho', defaultSlippage: 50, }, - privyConfig: { - type: 'privy', - appId: 'test-app-id', - appSecret: 'test-app-secret', + wallet: { + embeddedWalletConfig: { + provider: { + type: 'privy', + appId: 'test-app-id', + appSecret: 'test-app-secret', + }, + }, + smartWalletConfig: { + provider: { + type: 'default', + }, + }, }, }) @@ -128,10 +155,19 @@ describe('Verbs SDK - System Tests', () => { type: 'morpho', defaultSlippage: 50, }, - privyConfig: { - type: 'privy', - appId: 'test-app-id', - appSecret: 'test-app-secret', + wallet: { + embeddedWalletConfig: { + provider: { + type: 'privy', + appId: 'test-app-id', + appSecret: 'test-app-secret', + }, + }, + smartWalletConfig: { + provider: { + type: 'default', + }, + }, }, }) @@ -147,10 +183,19 @@ describe('Verbs SDK - System Tests', () => { type: 'morpho', defaultSlippage: 50, }, - privyConfig: { - type: 'privy', - appId: 'test-app-id', - appSecret: 'test-app-secret', + wallet: { + embeddedWalletConfig: { + provider: { + type: 'privy', + appId: 'test-app-id', + appSecret: 'test-app-secret', + }, + }, + smartWalletConfig: { + provider: { + type: 'default', + }, + }, }, }) diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index 8ba78a29..82176f5c 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -5,7 +5,12 @@ import { ChainManager } from '@/services/ChainManager.js' import type { LendProvider } from '@/types/lend.js' import type { VerbsConfig } from '@/types/verbs.js' +import type { EmbeddedWalletProvider } from './wallet/providers/base/EmbeddedWalletProvider.js' +import type { SmartWalletProvider } from './wallet/providers/base/SmartWalletProvider.js' +import { DefaultSmartWalletProvider } from './wallet/providers/DefaultSmartWalletProvider.js' +import { PrivyEmbeddedWalletProvider } from './wallet/providers/privy.js' import { WalletNamespace } from './wallet/WalletNamespace.js' +import { WalletProvider } from './wallet/WalletProvider.js' /** * Main Verbs SDK class @@ -15,9 +20,10 @@ export class Verbs { public readonly wallet: WalletNamespace private _chainManager: ChainManager private lendProvider?: LendProvider + private embeddedWalletProvider: EmbeddedWalletProvider + private smartWalletProvider: SmartWalletProvider constructor(config: VerbsConfig) { - this.wallet = new WalletNamespace() this._chainManager = new ChainManager( config.chains || [ { @@ -26,6 +32,7 @@ export class Verbs { }, ], ) + // Create lending provider if configured if (config.lend) { if (config.lend.type === 'morpho') { @@ -39,6 +46,39 @@ export class Verbs { ) } } + + if (config.wallet.embeddedWalletConfig.provider.type === 'privy') { + this.embeddedWalletProvider = new PrivyEmbeddedWalletProvider( + config.wallet.embeddedWalletConfig.provider.appId, + config.wallet.embeddedWalletConfig.provider.appSecret, + this.chainManager, + ) + } else { + throw new Error( + `Unsupported embedded wallet provider: ${config.wallet.embeddedWalletConfig.provider.type}`, + ) + } + + if ( + !config.wallet.smartWalletConfig || + config.wallet.smartWalletConfig.provider.type === 'default' + ) { + this.smartWalletProvider = new DefaultSmartWalletProvider( + this.chainManager, + this.lend, + ) + } else { + throw new Error( + `Unsupported smart wallet provider: ${config.wallet.smartWalletConfig.provider.type}`, + ) + } + + // Create unified wallet provider + const walletProvider = new WalletProvider( + this.embeddedWalletProvider, + this.smartWalletProvider, + ) + this.wallet = new WalletNamespace(walletProvider) } /** @@ -60,25 +100,3 @@ export class Verbs { return this._chainManager } } - -/** - * Initialize Verbs SDK - * @description Factory function to create a new Verbs SDK instance - * @param config - SDK configuration - * @returns Initialized Verbs SDK instance - */ -export function initVerbs(config: VerbsConfig) { - const verbs = new Verbs(config) - if (config.privyConfig) { - verbs.wallet.withPrivy( - config.privyConfig.appId, - config.privyConfig.appSecret, - verbs.chainManager, - ) - } - if (config.enableSmartWallets) { - verbs.wallet.withSmartWallet(verbs.chainManager, verbs.lend) - } - - return verbs -} diff --git a/packages/sdk/src/wallet/PrivyWallet.ts b/packages/sdk/src/wallet/PrivyWallet.ts index 0b03aa6a..3c023697 100644 --- a/packages/sdk/src/wallet/PrivyWallet.ts +++ b/packages/sdk/src/wallet/PrivyWallet.ts @@ -1,18 +1,18 @@ -import type { Address, Hash, Hex, LocalAccount, Quantity } from 'viem' +import type { Address, Hash, LocalAccount } from 'viem' import { toAccount } from 'viem/accounts' import type { ChainManager } from '@/services/ChainManager.js' +import { EmbeddedWallet } from '@/wallet/base/EmbeddedWallet.js' -import type { PrivyWalletProvider } from './providers/privy.js' +import type { PrivyEmbeddedWalletProvider } from './providers/privy.js' /** * Privy wallet implementation * @description Wallet implementation using Privy service */ -export class PrivyWallet { - public address: Address +export class PrivyWallet extends EmbeddedWallet { public walletId: string - private privyProvider: PrivyWalletProvider + private privyProvider: PrivyEmbeddedWalletProvider private chainManager: ChainManager /** * Create a new Privy wallet provider @@ -21,15 +21,15 @@ export class PrivyWallet { * @param verbs - Verbs instance for accessing configured providers */ constructor( - privyProvider: PrivyWalletProvider, + privyProvider: PrivyEmbeddedWalletProvider, chainManager: ChainManager, walletId: string, address: Address, ) { + super(address) this.privyProvider = privyProvider this.chainManager = chainManager this.walletId = walletId - this.address = address } /** @@ -74,57 +74,4 @@ export class PrivyWallet { }, }) } - - /** - * Sign a transaction without sending it - * @description Signs a transaction using Privy's wallet API but doesn't send it - * @param walletId - Wallet ID to use for signing - * @param transactionData - Transaction data to sign - * @returns Promise resolving to signed transaction - * @throws Error if transaction signing fails - */ - async signOnly(txParams: { - /** The address the transaction is sent from. Must be hexadecimal formatted. */ - from?: Hex - /** Destination address of the transaction. */ - to?: Hex - /** The nonce to be used for the transaction (hexadecimal or number). */ - nonce?: Quantity - /** (optional) The chain ID of network your transaction will be sent on. */ - chainId?: Quantity - /** (optional) Data to send to the receiving address, especially when calling smart contracts. Must be hexadecimal formatted. */ - data?: Hex - /** (optional) The value (in wei) be sent with the transaction (hexadecimal or number). */ - value?: Quantity - /** (optional) The EIP-2718 transction type (e.g. `2` for EIP-1559 transactions). */ - type?: 0 | 1 | 2 - /** (optional) The max units of gas that can be used by this transaction (hexadecimal or number). */ - gasLimit?: Quantity - /** (optional) The price (in wei) per unit of gas for this transaction (hexadecimal or number), for use in non EIP-1559 transactions (type 0 or 1). */ - gasPrice?: Quantity - /** (optional) The maxFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ - maxFeePerGas?: Quantity - /** (optional) The maxPriorityFeePerGas (hexadecimal or number) to be used in this transaction, for use in EIP-1559 (type 2) transactions. */ - maxPriorityFeePerGas?: Quantity - }): Promise { - try { - console.log( - `[PRIVY_PROVIDER] Complete tx params - Type: ${txParams.type}, Nonce: ${txParams.nonce}, Limit: ${txParams.gasLimit}, MaxFee: ${txParams.maxFeePerGas || 'fallback'}, Priority: ${txParams.maxPriorityFeePerGas || 'fallback'}`, - ) - - const response = - await this.privyProvider.privy.walletApi.ethereum.signTransaction({ - walletId: this.walletId, - transaction: txParams, - }) - - return response.signedTransaction - } catch (error) { - throw new Error( - `Failed to sign transaction for wallet ${this.walletId}: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - ) - } - } } diff --git a/packages/sdk/src/wallet/SmartWallet.ts b/packages/sdk/src/wallet/SmartWallet.ts index 517fce10..66481e38 100644 --- a/packages/sdk/src/wallet/SmartWallet.ts +++ b/packages/sdk/src/wallet/SmartWallet.ts @@ -23,19 +23,20 @@ import { parseLendParams, resolveAsset, } from '@/utils/assets.js' +import { SmartWallet } from '@/wallet/base/SmartWallet.js' /** * Smart Wallet Implementation * @description ERC-4337 compatible smart wallet that uses Coinbase Smart Account (https://github.com/coinbase/smart-wallet/blob/main/src/CoinbaseSmartWallet.sol). * Supports multi-owner wallets, gasless transactions via paymasters, and cross-chain operations. */ -export class SmartWallet { +export class DefaultSmartWallet extends SmartWallet { /** Array of wallet owners (Ethereum addresses or WebAuthn public keys) */ private owners: Array
/** Local account used for signing transactions and UserOperations */ - private signer: LocalAccount + private _signer: LocalAccount /** Index of the signer in the owners array (defaults to 0 if not specified) */ - private ownerIndex?: number + private signerOwnerIndex?: number /** Known deployment address of the wallet (if already deployed) */ private deploymentAddress?: Address /** Provider for lending market operations */ @@ -62,18 +63,23 @@ export class SmartWallet { chainManager: ChainManager, lendProvider: LendProvider, deploymentAddress?: Address, - ownerIndex?: number, + signerOwnerIndex?: number, nonce?: bigint, ) { + super() this.owners = owners - this.signer = signer - this.ownerIndex = ownerIndex + this._signer = signer + this.signerOwnerIndex = signerOwnerIndex this.deploymentAddress = deploymentAddress this.chainManager = chainManager this.lendProvider = lendProvider this.nonce = nonce } + get signer(): LocalAccount { + return this._signer + } + /** * Get the smart wallet address * @description Returns the deployment address if known, otherwise calculates the deterministic @@ -113,7 +119,7 @@ export class SmartWallet { ): ReturnType { return toCoinbaseSmartAccount({ address: this.deploymentAddress, - ownerIndex: this.ownerIndex, + ownerIndex: this.signerOwnerIndex, client: this.chainManager.getPublicClient(chainId), owners: [this.signer], nonce: this.nonce, diff --git a/packages/sdk/src/wallet/WalletNamespace.ts b/packages/sdk/src/wallet/WalletNamespace.ts index 3a1d3fc2..a3118a9e 100644 --- a/packages/sdk/src/wallet/WalletNamespace.ts +++ b/packages/sdk/src/wallet/WalletNamespace.ts @@ -1,20 +1,39 @@ -import type { ChainManager } from '@/services/ChainManager.js' -import type { LendProvider } from '@/types/lend.js' -import { PrivyWalletProvider } from '@/wallet/providers/privy.js' -import { SmartWalletProvider } from '@/wallet/providers/smartWallet.js' +import type { Address } from 'viem' +import type { WebAuthnAccount } from 'viem/account-abstraction' -// Wallet namespace that holds all providers +import type { WalletProvider } from '@/wallet/WalletProvider.js' + +/** + * Wallet namespace that provides unified wallet operations + * @description Provides access to wallet functionality through a single provider interface + */ export class WalletNamespace { - public privy?: PrivyWalletProvider - public smartWallet?: SmartWalletProvider + private provider: WalletProvider + + constructor(provider: WalletProvider) { + this.provider = provider + } + + get embeddedWalletProvider() { + return this.provider.embeddedWalletProvider + } + + get smartWalletProvider() { + return this.provider.smartWalletProvider + } - withPrivy(appId: string, appSecret: string, chainManager: ChainManager) { - this.privy = new PrivyWalletProvider(appId, appSecret, chainManager) - return this + // Convenience methods that delegate to the provider + async createWallet() { + return this.provider.createWallet() } - withSmartWallet(chainManager: ChainManager, lendProvider: LendProvider) { - this.smartWallet = new SmartWalletProvider(chainManager, lendProvider) - return this + async getWallet(params: { + walletId: string + owners?: Array
+ signerOwnerIndex?: number + walletAddress?: Address + nonce?: bigint + }) { + return this.provider.getWallet(params) } } diff --git a/packages/sdk/src/wallet/WalletProvider.ts b/packages/sdk/src/wallet/WalletProvider.ts new file mode 100644 index 00000000..9d35f556 --- /dev/null +++ b/packages/sdk/src/wallet/WalletProvider.ts @@ -0,0 +1,67 @@ +import type { LocalAccount } from 'viem' + +import type { GetWalletOptions } from '@/types/wallet.js' +import type { EmbeddedWalletProvider } from '@/wallet/providers/base/EmbeddedWalletProvider.js' +import type { SmartWalletProvider } from '@/wallet/providers/base/SmartWalletProvider.js' + +/** + * Unified Wallet Provider + * @description Main wallet provider that combines embedded wallet and smart wallet functionality. + * Provides a unified interface for all wallet operations while supporting pluggable providers. + */ +export class WalletProvider { + public readonly embeddedWalletProvider: EmbeddedWalletProvider + public readonly smartWalletProvider: SmartWalletProvider + + constructor( + embeddedWalletProvider: EmbeddedWalletProvider, + smartWalletProvider: SmartWalletProvider, + ) { + this.embeddedWalletProvider = embeddedWalletProvider + this.smartWalletProvider = smartWalletProvider + } + + async createWallet() { + const embeddedWallet = await this.embeddedWalletProvider.createWallet() + const signer = await embeddedWallet.signer() + return this.smartWalletProvider.createWallet({ + owners: [embeddedWallet.address], + signer, + }) + } + + /** + * Get an existing wallet + */ + async getWallet(params: GetWalletOptions) { + const { walletId } = params + const embeddedWallet = await this.embeddedWalletProvider.getWallet({ + walletId, + }) + if (!embeddedWallet) { + throw new Error('Embedded wallet not found') + } + const signer = await embeddedWallet.signer() + return this.getSmartWallet(params, signer) + } + + private async getSmartWallet(params: GetWalletOptions, signer: LocalAccount) { + const { + owners, + signerOwnerIndex, + walletAddress: walletAddressParam, + nonce, + } = params + const walletAddress = + walletAddressParam || + (await this.smartWalletProvider.getWalletAddress({ + owners: owners || [signer.address], + nonce, + })) + return this.smartWalletProvider.getWallet({ + walletAddress, + signer, + ownerIndex: signerOwnerIndex, + }) + } +} diff --git a/packages/sdk/src/wallet/base/EmbeddedWallet.ts b/packages/sdk/src/wallet/base/EmbeddedWallet.ts new file mode 100644 index 00000000..3a20fdb4 --- /dev/null +++ b/packages/sdk/src/wallet/base/EmbeddedWallet.ts @@ -0,0 +1,11 @@ +import type { Address, LocalAccount } from 'viem' + +export abstract class EmbeddedWallet { + public readonly address: Address + + constructor(address: Address) { + this.address = address + } + + abstract signer(): Promise +} diff --git a/packages/sdk/src/wallet/base/SmartWallet.ts b/packages/sdk/src/wallet/base/SmartWallet.ts new file mode 100644 index 00000000..16daf89e --- /dev/null +++ b/packages/sdk/src/wallet/base/SmartWallet.ts @@ -0,0 +1,32 @@ +import type { Address, Hash, LocalAccount } from 'viem' + +import type { SupportedChainId } from '@/constants/supportedChains.js' +import type { + LendOptions, + LendTransaction, + TransactionData, +} from '@/types/lend.js' +import type { TokenBalance } from '@/types/token.js' +import type { AssetIdentifier } from '@/utils/assets.js' + +export abstract class SmartWallet { + abstract signer: LocalAccount + abstract getAddress(): Promise
+ abstract getBalance(): Promise + abstract send( + transactionData: TransactionData, + chainId: SupportedChainId, + ): Promise + abstract lend( + amount: number, + asset: AssetIdentifier, + chainId: SupportedChainId, + marketId?: string, + options?: LendOptions, + ): Promise + abstract sendTokens( + amount: number, + asset: AssetIdentifier, + recipientAddress: Address, + ): Promise +} diff --git a/packages/sdk/src/wallet/providers/smartWallet.ts b/packages/sdk/src/wallet/providers/DefaultSmartWalletProvider.ts similarity index 91% rename from packages/sdk/src/wallet/providers/smartWallet.ts rename to packages/sdk/src/wallet/providers/DefaultSmartWalletProvider.ts index 33b117f5..91de9720 100644 --- a/packages/sdk/src/wallet/providers/smartWallet.ts +++ b/packages/sdk/src/wallet/providers/DefaultSmartWalletProvider.ts @@ -6,14 +6,15 @@ import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' import { smartWalletFactoryAddress } from '@/constants/addresses.js' import type { ChainManager } from '@/services/ChainManager.js' import type { LendProvider } from '@/types/lend.js' -import { SmartWallet } from '@/wallet/SmartWallet.js' +import { SmartWalletProvider } from '@/wallet/providers/base/SmartWalletProvider.js' +import { DefaultSmartWallet } from '@/wallet/SmartWallet.js' /** * Smart Wallet Provider * @description Factory for creating and managing Smart Wallet instances. * Handles wallet address prediction, creation, and retrieval using ERC-4337 account abstraction. */ -export class SmartWalletProvider { +export class DefaultSmartWalletProvider extends SmartWalletProvider { /** Manages supported blockchain networks */ private chainManager: ChainManager /** Provider for lending market operations */ @@ -26,6 +27,7 @@ export class SmartWalletProvider { * @param lendProvider - Provider for lending market operations */ constructor(chainManager: ChainManager, lendProvider: LendProvider) { + super() this.chainManager = chainManager this.lendProvider = lendProvider } @@ -43,9 +45,9 @@ export class SmartWalletProvider { owners: Array
signer: LocalAccount nonce?: bigint - }): Promise { + }): Promise { const { owners, signer, nonce } = params - return new SmartWallet( + return new DefaultSmartWallet( owners, signer, this.chainManager, @@ -64,7 +66,7 @@ export class SmartWalletProvider { * @param params.nonce - Nonce for address generation (defaults to 0) * @returns Promise resolving to the predicted wallet address */ - async getAddress(params: { + async getWalletAddress(params: { owners: Array
nonce?: bigint }) { @@ -101,9 +103,9 @@ export class SmartWalletProvider { walletAddress: Address signer: LocalAccount ownerIndex?: number - }): SmartWallet { + }): DefaultSmartWallet { const { walletAddress, signer, ownerIndex } = params - return new SmartWallet( + return new DefaultSmartWallet( [signer.address], signer, this.chainManager, diff --git a/packages/sdk/src/wallet/providers/base/EmbeddedWalletProvider.ts b/packages/sdk/src/wallet/providers/base/EmbeddedWalletProvider.ts new file mode 100644 index 00000000..ce6d5e4d --- /dev/null +++ b/packages/sdk/src/wallet/providers/base/EmbeddedWalletProvider.ts @@ -0,0 +1,10 @@ +import type { EmbeddedWallet } from '@/wallet/base/EmbeddedWallet.js' + +/** + * Base embedded wallet provider interface + * @description Abstract interface for embedded wallet providers (Privy, Dynamic, etc.) + */ +export abstract class EmbeddedWalletProvider { + abstract createWallet(): Promise + abstract getWallet(params: { walletId: string }): Promise +} diff --git a/packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts b/packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts new file mode 100644 index 00000000..898b629c --- /dev/null +++ b/packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts @@ -0,0 +1,27 @@ +import type { Address, LocalAccount } from 'viem' +import type { WebAuthnAccount } from 'viem/account-abstraction' + +import type { SmartWallet } from '@/wallet/base/SmartWallet.js' + +/** + * Base smart wallet provider interface + * @description Abstract interface for smart wallet providers (Native, etc.) + */ +export abstract class SmartWalletProvider { + abstract createWallet(params: { + owners: Array
+ signer: LocalAccount + nonce?: bigint + }): Promise + + abstract getWallet(params: { + walletAddress: Address + signer: LocalAccount + ownerIndex?: number + }): SmartWallet + + abstract getWalletAddress(params: { + owners: Array
+ nonce?: bigint + }): Promise
+} diff --git a/packages/sdk/src/wallet/providers/privy.ts b/packages/sdk/src/wallet/providers/privy.ts index e2bae1e2..13daf1f4 100644 --- a/packages/sdk/src/wallet/providers/privy.ts +++ b/packages/sdk/src/wallet/providers/privy.ts @@ -2,14 +2,25 @@ import { PrivyClient } from '@privy-io/server-auth' import { getAddress } from 'viem' import type { ChainManager } from '@/services/ChainManager.js' -import type { GetAllWalletsOptions } from '@/types/wallet.js' import { PrivyWallet } from '@/wallet/PrivyWallet.js' +import { EmbeddedWalletProvider } from '@/wallet/providers/base/EmbeddedWalletProvider.js' + +/** + * Options for getting all wallets + * @description Parameters for filtering and paginating wallet results + */ +export interface PrivyProviderGetAllWalletsOptions { + /** Maximum number of wallets to return */ + limit?: number + /** Cursor for pagination */ + cursor?: string +} /** * Privy wallet provider implementation * @description Wallet provider implementation using Privy service */ -export class PrivyWalletProvider { +export class PrivyEmbeddedWalletProvider extends EmbeddedWalletProvider { public privy: PrivyClient private chainManager: ChainManager @@ -20,6 +31,7 @@ export class PrivyWalletProvider { * @param verbs - Verbs instance for accessing configured providers */ constructor(appId: string, appSecret: string, chainManager: ChainManager) { + super() this.privy = new PrivyClient(appId, appSecret) this.chainManager = chainManager } @@ -49,15 +61,16 @@ export class PrivyWalletProvider { } /** - * Get wallet by user ID via Privy + * Get wallet by wallet ID via Privy * @description Retrieves wallet information from Privy service - * @param userId - User identifier - * @returns Promise resolving to wallet or null if not found + * @param params - Parameters containing walletId + * @returns Promise resolving to wallet */ - async getWallet(userId: string): Promise { + async getWallet(params: { walletId: string }): Promise { try { - // TODO: Implement proper user-to-wallet lookup - const wallet = await this.privy.walletApi.getWallet({ id: userId }) + const wallet = await this.privy.walletApi.getWallet({ + id: params.walletId, + }) const walletInstance = new PrivyWallet( this, @@ -67,7 +80,7 @@ export class PrivyWalletProvider { ) return walletInstance } catch { - return null + throw new Error(`Failed to get wallet with id: ${params.walletId}`) } } @@ -77,7 +90,9 @@ export class PrivyWalletProvider { * @param options - Optional parameters for filtering and pagination * @returns Promise resolving to array of wallets */ - async getAllWallets(options?: GetAllWalletsOptions): Promise { + async getAllWallets( + options?: PrivyProviderGetAllWalletsOptions, + ): Promise { try { const response = await this.privy.walletApi.getWallets({ limit: options?.limit, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76171e1b..1b2f8d7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,6 +133,9 @@ importers: '@eth-optimism/verbs-sdk': specifier: workspace:* version: link:../../sdk + '@eth-optimism/verbs-service': + specifier: workspace:* + version: link:../backend react: specifier: ^18 version: 18.3.1 From 6f8158ce131ebf2cced2792ca3e78c27dcb01d28 Mon Sep 17 00:00:00 2001 From: tre Date: Tue, 26 Aug 2025 17:03:04 -0700 Subject: [PATCH 17/39] remove find-vault script --- packages/sdk/package.json | 3 +- packages/sdk/scripts/find-vault.ts | 82 ------------------------------ 2 files changed, 1 insertion(+), 84 deletions(-) delete mode 100644 packages/sdk/scripts/find-vault.ts diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 14b7929c..96104698 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -29,8 +29,7 @@ "lint:fix": "eslint \"**/*.{ts,tsx}\" --fix && prettier \"**/*.{ts,tsx}\" --write --loglevel=warn", "test": "vitest --run --project unit", "test:supersim": "vitest --run --project supersim", - "typecheck": "tsc --noEmit --emitDeclarationOnly false", - "find-vault": "tsx scripts/find-vault.ts" + "typecheck": "tsc --noEmit --emitDeclarationOnly false" }, "keywords": [ "verbs", diff --git a/packages/sdk/scripts/find-vault.ts b/packages/sdk/scripts/find-vault.ts deleted file mode 100644 index 2846fce0..00000000 --- a/packages/sdk/scripts/find-vault.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { createPublicClient, http, parseAbiItem } from 'viem' -import { baseSepolia } from 'viem/chains' - -const USDC = '0x036CbD53842c5426634e7929541eC2318f3dCF7e' // Base Sepolia -const FACTORY = '0x2c3FE6D71F8d54B063411Abb446B49f13725F784' // MetaMorpho Factory v1.1 on Base Sepolia - -async function findUSDCVaults() { - const client = createPublicClient({ - chain: baseSepolia, - transport: http(), - }) - - const createEvt = parseAbiItem( - 'event CreateMetaMorpho(address indexed metaMorpho, address indexed caller, address initialOwner, uint256 initialTimelock, address indexed asset, string name, string symbol, bytes32 salt)', - ) - - console.log('Searching for USDC vaults on Base Sepolia...') - console.log(`USDC address: ${USDC}`) - console.log(`Factory address: ${FACTORY}`) - - try { - // Get latest block number - const latestBlock = 28898281n - - const CHUNK_SIZE = 10000n // Search in chunks of 10k blocks - const MAX_CHUNKS = 100 // Only search the last 100k blocks - const usdcVaults: any[] = [] - - for (let i = 0; i < MAX_CHUNKS && usdcVaults.length < 5; i++) { - const toBlock = latestBlock - BigInt(i) * CHUNK_SIZE - const fromBlock = toBlock - CHUNK_SIZE + 1n - - if (fromBlock < 0n) break - - console.log(`Searching blocks ${fromBlock} to ${toBlock}...`) - - try { - const logs = await client.getLogs({ - address: FACTORY, - event: createEvt, - fromBlock, - toBlock, - }) - - const chunkUsdcVaults = logs.filter( - (l) => l.args.asset?.toLowerCase() === USDC.toLowerCase(), - ) - usdcVaults.push(...chunkUsdcVaults) - - if (chunkUsdcVaults.length > 0) { - console.log( - `Found ${chunkUsdcVaults.length} USDC vaults in this chunk`, - ) - } - } catch (chunkError) { - console.log( - `Error searching chunk ${fromBlock}-${toBlock}:`, - chunkError.message, - ) - continue - } - } - - console.log(`\nTotal found: ${usdcVaults.length} USDC vaults:`) - usdcVaults.forEach((vault, index) => { - console.log(`${index + 1}. ${vault.args.metaMorpho}`) - console.log(` Name: ${vault.args.name}`) - console.log(` Symbol: ${vault.args.symbol}`) - console.log(` Creator: ${vault.args.creator}`) - console.log(` Block: ${vault.blockNumber}`) - console.log('') - }) - - if (usdcVaults.length === 0) { - console.log('No USDC vaults found in recent blocks.') - } - } catch (error) { - console.error('Error fetching vault data:', error) - } -} - -findUSDCVaults().catch(console.error) From d8ddbe29218294500865413ea48d5a04e5898636 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 15:30:03 -0700 Subject: [PATCH 18/39] improve the WalletProvider to make it moer expressive --- packages/demo/backend/src/services/wallet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index 4192d1ed..70965612 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -28,7 +28,7 @@ export async function createWallet(): Promise<{ smartWalletAddress: string }> { const verbs = getVerbs() - const wallet = await verbs.wallet.createWallet() + const wallet = await verbs.wallet.createWalletWithEmbeddedSigner() const smartWalletAddress = await wallet.getAddress() return { privyAddress: wallet.signer.address, @@ -40,7 +40,7 @@ export async function getWallet(userId: string): Promise<{ wallet: SmartWallet }> { const verbs = getVerbs() - const wallet = await verbs.wallet.getWallet({ + const wallet = await verbs.wallet.getSmartWalletWithEmbeddedSigner({ walletId: userId, }) return { wallet } From c725b93154f63e367cccc5124ebe931189450f80 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 15:53:20 -0700 Subject: [PATCH 19/39] improvements --- packages/demo/backend/src/config/verbs.ts | 4 +- packages/demo/backend/src/services/wallet.ts | 2 +- packages/sdk/package.json | 4 +- packages/sdk/src/test/MockPrivyClient.ts | 119 +++++++++++++ packages/sdk/src/types/verbs.ts | 8 +- packages/sdk/src/types/wallet.ts | 49 +++++- packages/sdk/src/verbs.test.ts | 31 ++-- packages/sdk/src/verbs.ts | 4 +- packages/sdk/src/wallet/PrivyWallet.ts | 19 +-- packages/sdk/src/wallet/WalletNamespace.ts | 125 ++++++++++++-- packages/sdk/src/wallet/WalletProvider.ts | 160 ++++++++++++++++-- packages/sdk/src/wallet/base/SmartWallet.ts | 2 + .../providers/base/SmartWalletProvider.ts | 29 ++++ packages/sdk/src/wallet/providers/privy.ts | 22 +-- pnpm-lock.yaml | 62 +------ 15 files changed, 499 insertions(+), 141 deletions(-) create mode 100644 packages/sdk/src/test/MockPrivyClient.ts diff --git a/packages/demo/backend/src/config/verbs.ts b/packages/demo/backend/src/config/verbs.ts index c52aa29f..08881f98 100644 --- a/packages/demo/backend/src/config/verbs.ts +++ b/packages/demo/backend/src/config/verbs.ts @@ -1,4 +1,5 @@ import { Verbs, type VerbsConfig } from '@eth-optimism/verbs-sdk' +import { PrivyClient } from '@privy-io/server-auth' import { baseSepolia, unichain } from 'viem/chains' import { env } from './env.js' @@ -11,8 +12,7 @@ export function createVerbsConfig(): VerbsConfig { embeddedWalletConfig: { provider: { type: 'privy', - appId: env.PRIVY_APP_ID, - appSecret: env.PRIVY_APP_SECRET, + privyClient: new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET), }, }, smartWalletConfig: { diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index 70965612..a4c0a00a 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -61,7 +61,7 @@ export async function getAllWallets( owners: [privyWallet.address], }) const signer = await privyWallet.signer() - const wallet = await ( + const wallet = ( verbs.wallet.smartWalletProvider as SmartWalletProvider ).getWallet({ walletAddress, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 96104698..7eed3b53 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -43,9 +43,11 @@ "@morpho-org/blue-sdk": "^4.5.1", "@morpho-org/blue-sdk-viem": "^3.1.1", "@morpho-org/morpho-ts": "^2.4.1", - "@privy-io/server-auth": "latest", "viem": "^2.24.1" }, + "peerDependencies": { + "@privy-io/server-auth": ">=1.28.0" + }, "devDependencies": { "@types/node": "^18", "dotenv": "^16.4.5", diff --git a/packages/sdk/src/test/MockPrivyClient.ts b/packages/sdk/src/test/MockPrivyClient.ts new file mode 100644 index 00000000..6e81af0b --- /dev/null +++ b/packages/sdk/src/test/MockPrivyClient.ts @@ -0,0 +1,119 @@ +import type { PrivyClient } from '@privy-io/server-auth' +import type { Address } from 'viem' + +/** + * Mock Privy Client for testing + * @description Provides a mock implementation of PrivyClient for testing purposes + */ +export class MockPrivyClient { + private mockWallets = new Map() + private walletCounter = 0 + + constructor( + public appId: string, + public appSecret: string, + ) {} + + get walletApi() { + return { + createWallet: async (params: { chainType: string }) => { + const walletId = `mock-wallet-${++this.walletCounter}` + const address = + `0x${walletId.replace(/[^0-9]/g, '').padEnd(40, '0')}` as Address + + const wallet = new MockWallet(walletId, address) + this.mockWallets.set(walletId, wallet) + + return { + id: walletId, + address: address, + chainType: params.chainType, + } + }, + + getWallet: async (params: { id: string }) => { + const wallet = this.mockWallets.get(params.id) + if (!wallet) { + throw new Error(`Wallet ${params.id} not found`) + } + + return { + id: wallet.id, + address: wallet.address, + chainType: 'ethereum', + } + }, + + getWallets: async (params?: { limit?: number }) => { + const wallets = Array.from(this.mockWallets.values()) + const limit = params?.limit || wallets.length + + return { + data: wallets.slice(0, limit).map((wallet) => ({ + id: wallet.id, + address: wallet.address, + chainType: 'ethereum', + })), + } + }, + + ethereum: { + signMessage: async (params: { walletId: string; message: string }) => { + const wallet = this.mockWallets.get(params.walletId) + if (!wallet) { + throw new Error(`Wallet ${params.walletId} not found`) + } + + // Mock signature - deterministic based on message + const mockSig = `0x${'a'.repeat(128)}${params.message.length.toString(16).padStart(2, '0')}` + return { signature: mockSig } + }, + + secp256k1Sign: async (params: { walletId: string; hash: string }) => { + const wallet = this.mockWallets.get(params.walletId) + if (!wallet) { + throw new Error(`Wallet ${params.walletId} not found`) + } + + // Mock signature - deterministic based on hash + const mockSig = `0x${'b'.repeat(128)}${params.hash.slice(-2)}` + return { signature: mockSig } + }, + + signTransaction: async (params: { + walletId: string + transaction: any + }) => { + const wallet = this.mockWallets.get(params.walletId) + if (!wallet) { + throw new Error(`Wallet ${params.walletId} not found`) + } + + // Mock signed transaction + const mockSignedTx = `0x${'c'.repeat(200)}` + return { signedTransaction: mockSignedTx } + }, + }, + } + } +} + +class MockWallet { + constructor( + public id: string, + public address: Address, + ) {} +} + +/** + * Create a mock Privy client cast as PrivyClient type + * @param appId - Mock app ID + * @param appSecret - Mock app secret + * @returns MockPrivyClient cast as PrivyClient + */ +export function createMockPrivyClient( + appId: string, + appSecret: string, +): PrivyClient { + return new MockPrivyClient(appId, appSecret) as unknown as PrivyClient +} diff --git a/packages/sdk/src/types/verbs.ts b/packages/sdk/src/types/verbs.ts index d683cfc7..1290ac02 100644 --- a/packages/sdk/src/types/verbs.ts +++ b/packages/sdk/src/types/verbs.ts @@ -1,3 +1,5 @@ +import type { PrivyClient } from '@privy-io/server-auth' + import type { ChainConfig } from '@/types/chain.js' import type { LendConfig } from './lend.js' @@ -68,8 +70,6 @@ export type EmbeddedWalletProviderConfig = PrivyEmbeddedWalletProviderConfig export interface PrivyEmbeddedWalletProviderConfig { /** Embedded wallet provider type */ type: 'privy' - /** Privy app ID */ - appId: string - /** Privy app secret */ - appSecret: string + /** Privy client instance */ + privyClient: PrivyClient } diff --git a/packages/sdk/src/types/wallet.ts b/packages/sdk/src/types/wallet.ts index 175cd4d6..75ae9214 100644 --- a/packages/sdk/src/types/wallet.ts +++ b/packages/sdk/src/types/wallet.ts @@ -1,14 +1,53 @@ -import type { Address } from 'viem' +import type { Address, LocalAccount } from 'viem' import type { WebAuthnAccount } from 'viem/account-abstraction' /** - * Options for getting a wallet - * @description Parameters for getting a wallet + * Options for creating a smart wallet + * @description Parameters for creating a new smart wallet with specified owners and signer */ -export type GetWalletOptions = { - walletId: string +export type CreateSmartWalletOptions = { + owners: Array
+ signer: LocalAccount + nonce?: bigint +} + +/** + * Options for creating a wallet with embedded signer + * @description Parameters for creating both embedded and smart wallets, with embedded wallet automatically added as signer + */ +export type CreateWalletWithEmbeddedSignerOptions = { owners?: Array
+ embeddedWalletIndex?: number + nonce?: bigint +} + +/** + * Options for retrieving a smart wallet with provided signer + * @description Parameters for getting an existing smart wallet using a provided LocalAccount signer + */ +export type GetSmartWalletOptions = { + signer: LocalAccount + deploymentOwners?: Array
signerOwnerIndex?: number walletAddress?: Address nonce?: bigint } + +/** + * Options for retrieving an embedded wallet + * @description Parameters for getting an existing embedded wallet + */ +export type GetEmbeddedWalletOptions = { + walletId: string +} + +/** + * Options for retrieving a smart wallet with embedded wallet signer + * @description Parameters for getting an existing smart wallet using an embedded wallet as signer. + * If neither walletAddress nor deploymentOwners is provided, defaults to using the embedded wallet as single owner. + */ +export type GetSmartWalletWithEmbeddedSignerOptions = Omit< + GetSmartWalletOptions, + 'signer' +> & + GetEmbeddedWalletOptions diff --git a/packages/sdk/src/verbs.test.ts b/packages/sdk/src/verbs.test.ts index acfa95f1..2150a962 100644 --- a/packages/sdk/src/verbs.test.ts +++ b/packages/sdk/src/verbs.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' +import { createMockPrivyClient } from './test/MockPrivyClient.js' import { externalTest } from './utils/test.js' import { Verbs } from './verbs.js' @@ -20,8 +21,10 @@ describe('Verbs SDK - System Tests', () => { embeddedWalletConfig: { provider: { type: 'privy', - appId: 'test-app-id', - appSecret: 'test-app-secret', + privyClient: createMockPrivyClient( + 'test-app-id', + 'test-app-secret', + ), }, }, smartWalletConfig: { @@ -89,8 +92,10 @@ describe('Verbs SDK - System Tests', () => { embeddedWalletConfig: { provider: { type: 'privy', - appId: 'test-app-id', - appSecret: 'test-app-secret', + privyClient: createMockPrivyClient( + 'test-app-id', + 'test-app-secret', + ), }, }, smartWalletConfig: { @@ -129,8 +134,10 @@ describe('Verbs SDK - System Tests', () => { embeddedWalletConfig: { provider: { type: 'privy', - appId: 'test-app-id', - appSecret: 'test-app-secret', + privyClient: createMockPrivyClient( + 'test-app-id', + 'test-app-secret', + ), }, }, smartWalletConfig: { @@ -159,8 +166,10 @@ describe('Verbs SDK - System Tests', () => { embeddedWalletConfig: { provider: { type: 'privy', - appId: 'test-app-id', - appSecret: 'test-app-secret', + privyClient: createMockPrivyClient( + 'test-app-id', + 'test-app-secret', + ), }, }, smartWalletConfig: { @@ -187,8 +196,10 @@ describe('Verbs SDK - System Tests', () => { embeddedWalletConfig: { provider: { type: 'privy', - appId: 'test-app-id', - appSecret: 'test-app-secret', + privyClient: createMockPrivyClient( + 'test-app-id', + 'test-app-secret', + ), }, }, smartWalletConfig: { diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index 82176f5c..93716c20 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -49,9 +49,7 @@ export class Verbs { if (config.wallet.embeddedWalletConfig.provider.type === 'privy') { this.embeddedWalletProvider = new PrivyEmbeddedWalletProvider( - config.wallet.embeddedWalletConfig.provider.appId, - config.wallet.embeddedWalletConfig.provider.appSecret, - this.chainManager, + config.wallet.embeddedWalletConfig.provider.privyClient, ) } else { throw new Error( diff --git a/packages/sdk/src/wallet/PrivyWallet.ts b/packages/sdk/src/wallet/PrivyWallet.ts index 3c023697..c08f2aaa 100644 --- a/packages/sdk/src/wallet/PrivyWallet.ts +++ b/packages/sdk/src/wallet/PrivyWallet.ts @@ -1,34 +1,25 @@ +import type { PrivyClient } from '@privy-io/server-auth' import type { Address, Hash, LocalAccount } from 'viem' import { toAccount } from 'viem/accounts' -import type { ChainManager } from '@/services/ChainManager.js' import { EmbeddedWallet } from '@/wallet/base/EmbeddedWallet.js' -import type { PrivyEmbeddedWalletProvider } from './providers/privy.js' - /** * Privy wallet implementation * @description Wallet implementation using Privy service */ export class PrivyWallet extends EmbeddedWallet { public walletId: string - private privyProvider: PrivyEmbeddedWalletProvider - private chainManager: ChainManager + private privyClient: PrivyClient /** * Create a new Privy wallet provider * @param appId - Privy application ID * @param appSecret - Privy application secret * @param verbs - Verbs instance for accessing configured providers */ - constructor( - privyProvider: PrivyEmbeddedWalletProvider, - chainManager: ChainManager, - walletId: string, - address: Address, - ) { + constructor(privyClient: PrivyClient, walletId: string, address: Address) { super(address) - this.privyProvider = privyProvider - this.chainManager = chainManager + this.privyClient = privyClient this.walletId = walletId } @@ -41,7 +32,7 @@ export class PrivyWallet extends EmbeddedWallet { * @throws Error if wallet retrieval fails or signing operations are not supported */ async signer(): Promise { - const privy = this.privyProvider.privy + const privy = this.privyClient const walletId = this.walletId const privyWallet = await privy.walletApi.getWallet({ id: walletId, diff --git a/packages/sdk/src/wallet/WalletNamespace.ts b/packages/sdk/src/wallet/WalletNamespace.ts index a3118a9e..296cc6cc 100644 --- a/packages/sdk/src/wallet/WalletNamespace.ts +++ b/packages/sdk/src/wallet/WalletNamespace.ts @@ -1,6 +1,12 @@ -import type { Address } from 'viem' -import type { WebAuthnAccount } from 'viem/account-abstraction' - +import type { + CreateSmartWalletOptions, + CreateWalletWithEmbeddedSignerOptions, + GetEmbeddedWalletOptions, + GetSmartWalletOptions, + GetSmartWalletWithEmbeddedSignerOptions, +} from '@/types/wallet.js' +import type { EmbeddedWallet } from '@/wallet/base/EmbeddedWallet.js' +import type { SmartWallet } from '@/wallet/base/SmartWallet.js' import type { WalletProvider } from '@/wallet/WalletProvider.js' /** @@ -14,26 +20,117 @@ export class WalletNamespace { this.provider = provider } + /** + * Get direct access to the embedded wallet provider + * @description Provides direct access to the underlying embedded wallet provider when + * advanced functionality beyond the unified interface is needed + * @returns The configured embedded wallet provider instance + */ get embeddedWalletProvider() { return this.provider.embeddedWalletProvider } + /** + * Get direct access to the smart wallet provider + * @description Provides direct access to the underlying smart wallet provider when + * advanced functionality beyond the unified interface is needed + * @returns The configured smart wallet provider instance + */ get smartWalletProvider() { return this.provider.smartWalletProvider } - // Convenience methods that delegate to the provider - async createWallet() { - return this.provider.createWallet() + /** + * Create a new embedded wallet + * @description Creates only an embedded wallet using the configured embedded wallet provider. + * @returns Promise resolving to the created embedded wallet instance + */ + async createEmbeddedWallet(): Promise { + return this.provider.createEmbeddedWallet() + } + + /** + * Create a new smart wallet + * @description Creates only a smart wallet using the configured smart wallet provider. + * This is useful when you already have a signer and want to create a smart wallet without + * creating an embedded wallet. You must provide your own signer and owners array. + * @param params - Smart wallet creation parameters + * @param params.owners - Array of owners for the smart wallet (addresses or WebAuthn public keys) + * @param params.signer - Local account used for signing transactions + * @param params.nonce - Optional nonce for smart wallet address generation (defaults to 0) + * @returns Promise resolving to the created smart wallet instance + */ + async createSmartWallet( + params: CreateSmartWalletOptions, + ): Promise { + return this.provider.createSmartWallet(params) + } + + /** + * Create a new smart wallet with embedded wallet as signer + * @description Creates both an embedded wallet and a smart wallet, with the embedded wallet + * automatically added as one of the owners/signers of the smart wallet. + * @param params - Optional wallet creation parameters + * @param params.owners - Optional array of additional owners for the smart wallet. The embedded wallet will be added to this array at the specified index. + * @param params.embeddedWalletIndex - Optional index where the embedded wallet should be inserted in the owners array. If not specified, embedded wallet is added to the end of the array. + * @param params.nonce - Optional nonce for smart wallet address generation (defaults to 0) + * @returns Promise resolving to the created smart wallet instance + */ + async createWalletWithEmbeddedSigner( + params?: CreateWalletWithEmbeddedSignerOptions, + ): Promise { + return this.provider.createWalletWithEmbeddedSigner(params) + } + + /** + * Get an existing smart wallet using embedded wallet as signer + * @description Retrieves an embedded wallet by walletId and uses it as the signer to get + * the corresponding smart wallet. If neither walletAddress nor deploymentOwners is provided, + * defaults to using the embedded wallet as the single owner. This is useful when you have + * an embedded wallet ID and want to access the associated smart wallet functionality. + * @param params - Wallet retrieval parameters + * @param params.walletId - ID of the embedded wallet to use as signer + * @param params.deploymentOwners - Optional array of original deployment owners for smart wallet address calculation. If not provided and walletAddress is also not provided, defaults to using the embedded wallet as single owner. + * @param params.signerOwnerIndex - Current index of the signer in the smart wallet's current owners array (used for transaction signing). Defaults to 0 if not specified. This may differ from the original deployment index if owners have been modified. + * @param params.walletAddress - Optional explicit smart wallet address (skips address calculation) + * @param params.nonce - Optional nonce used during smart wallet creation + * @returns Promise resolving to the smart wallet instance with embedded wallet as signer + * @throws Error if embedded wallet is not found + */ + async getSmartWalletWithEmbeddedSigner( + params: GetSmartWalletWithEmbeddedSignerOptions, + ) { + return this.provider.getSmartWalletWithEmbeddedSigner(params) + } + + /** + * Get an existing embedded wallet + * @description Retrieves an embedded wallet by walletId. This is useful when you have an embedded wallet ID and + * want to access the associated embedded wallet functionality. + * @param params - Wallet retrieval parameters + * @param params.walletId - ID of the embedded wallet to retrieve + * @returns Promise resolving to the embedded wallet instance + */ + async getEmbeddedWallet(params: GetEmbeddedWalletOptions) { + return this.provider.getEmbeddedWallet(params) } - async getWallet(params: { - walletId: string - owners?: Array
- signerOwnerIndex?: number - walletAddress?: Address - nonce?: bigint - }) { - return this.provider.getWallet(params) + /** + * Get an existing smart wallet with a provided signer + * @description Retrieves a smart wallet using a directly provided signer. This is useful when + * you already have a LocalAccount signer and want to access an existing smart wallet without + * going through the embedded wallet provider. Use this instead of getSmartWalletWithEmbeddedSigner + * when you have direct control over the signer. + * @param signer - Local account to use for signing transactions on the smart wallet + * @param getWalletParams - Wallet retrieval parameters + * @param getWalletParams.deploymentOwners - Array of original deployment owners for smart wallet address calculation. Required if walletAddress not provided. Must match the exact owners array used during wallet deployment. + * @param getWalletParams.signerOwnerIndex - Current index of the signer in the smart wallet's current owners array (used for transaction signing). Defaults to 0 if not specified. This may differ from the original deployment index if owners have been modified. + * @param getWalletParams.walletAddress - Optional explicit smart wallet address (skips address calculation) + * @param getWalletParams.nonce - Optional nonce used during smart wallet creation + * @returns Promise resolving to the smart wallet instance with the provided signer + * @throws Error if neither walletAddress nor deploymentOwners provided + */ + async getSmartWallet(params: GetSmartWalletOptions) { + return this.provider.getSmartWallet(params) } } diff --git a/packages/sdk/src/wallet/WalletProvider.ts b/packages/sdk/src/wallet/WalletProvider.ts index 9d35f556..5c3d78e1 100644 --- a/packages/sdk/src/wallet/WalletProvider.ts +++ b/packages/sdk/src/wallet/WalletProvider.ts @@ -1,6 +1,15 @@ -import type { LocalAccount } from 'viem' +import type { Address } from 'viem' +import type { WebAuthnAccount } from 'viem/account-abstraction' -import type { GetWalletOptions } from '@/types/wallet.js' +import type { + CreateSmartWalletOptions, + CreateWalletWithEmbeddedSignerOptions, + GetEmbeddedWalletOptions, + GetSmartWalletOptions, + GetSmartWalletWithEmbeddedSignerOptions, +} from '@/types/wallet.js' +import type { EmbeddedWallet } from '@/wallet/base/EmbeddedWallet.js' +import type { SmartWallet } from '@/wallet/base/SmartWallet.js' import type { EmbeddedWalletProvider } from '@/wallet/providers/base/EmbeddedWalletProvider.js' import type { SmartWalletProvider } from '@/wallet/providers/base/SmartWalletProvider.js' @@ -21,20 +30,91 @@ export class WalletProvider { this.smartWalletProvider = smartWalletProvider } - async createWallet() { + /** + * Create a new embedded wallet + * @description Creates only an embedded wallet using the configured embedded wallet provider. + * @returns Promise resolving to the created embedded wallet instance + */ + async createEmbeddedWallet(): Promise { + return this.embeddedWalletProvider.createWallet() + } + + /** + * Create a new smart wallet + * @description Creates only a smart wallet using the configured smart wallet provider. + * This is useful when you already have a signer and want to create a smart wallet without + * creating an embedded wallet. You must provide your own signer and owners array. + * @param params - Smart wallet creation parameters + * @param params.owners - Array of owners for the smart wallet (addresses or WebAuthn public keys) + * @param params.signer - Local account used for signing transactions + * @param params.nonce - Optional nonce for smart wallet address generation (defaults to 0) + * @returns Promise resolving to the created smart wallet instance + */ + async createSmartWallet( + params: CreateSmartWalletOptions, + ): Promise { + const { owners, signer, nonce } = params + + return this.smartWalletProvider.createWallet({ + owners, + signer, + nonce, + }) + } + + /** + * Create a new smart wallet with embedded wallet as signer + * @description Creates both an embedded wallet and a smart wallet, with the embedded wallet + * automatically added as one of the owners/signers of the smart wallet. + * @param params - Optional wallet creation parameters + * @param params.owners - Optional array of additional owners for the smart wallet. The embedded wallet will be added to this array at the specified index. + * @param params.embeddedWalletIndex - Optional index where the embedded wallet should be inserted in the owners array. If not specified, embedded wallet is added to the end of the array. + * @param params.nonce - Optional nonce for smart wallet address generation (defaults to 0) + * @returns Promise resolving to the created smart wallet instance + */ + async createWalletWithEmbeddedSigner( + params?: CreateWalletWithEmbeddedSignerOptions, + ): Promise { + const { owners: ownersParam, embeddedWalletIndex, nonce } = params || {} const embeddedWallet = await this.embeddedWalletProvider.createWallet() const signer = await embeddedWallet.signer() + + let owners: Array
+ if (ownersParam) { + owners = [...ownersParam] // Create a copy to avoid mutating the original + const insertIndex = embeddedWalletIndex ?? owners.length // Default to end if not specified + owners.splice(insertIndex, 0, embeddedWallet.address) // Insert embedded wallet at specified index + } else { + owners = [embeddedWallet.address] // Default to just the embedded wallet + } + return this.smartWalletProvider.createWallet({ - owners: [embeddedWallet.address], + owners, signer, + nonce, }) } /** - * Get an existing wallet + * Get an existing smart wallet using embedded wallet as signer + * @description Retrieves an embedded wallet by walletId and uses it as the signer to get + * the corresponding smart wallet. This is useful when you have + * an embedded wallet ID and want to access the associated smart wallet functionality. + * @dev If neither walletAddress nor deploymentOwners is provided, + * defaults to using the embedded wallet as the single owner. + * @param params - Wallet retrieval parameters + * @param params.walletId - ID of the embedded wallet to use as signer + * @param params.deploymentOwners - Optional array of original deployment owners for smart wallet address calculation. If not provided and walletAddress is also not provided, defaults to using the embedded wallet as single owner. + * @param params.signerOwnerIndex - Current index of the signer in the smart wallet's current owners array (used for transaction signing). Defaults to 0 if not specified. This may differ from the original deployment index if owners have been modified. + * @param params.walletAddress - Optional explicit smart wallet address (skips address calculation) + * @param params.nonce - Optional nonce used during smart wallet creation + * @returns Promise resolving to the smart wallet instance with embedded wallet as signer + * @throws Error if embedded wallet is not found */ - async getWallet(params: GetWalletOptions) { - const { walletId } = params + async getSmartWalletWithEmbeddedSigner( + params: GetSmartWalletWithEmbeddedSignerOptions, + ) { + const { walletId, deploymentOwners, walletAddress } = params const embeddedWallet = await this.embeddedWalletProvider.getWallet({ walletId, }) @@ -42,26 +122,82 @@ export class WalletProvider { throw new Error('Embedded wallet not found') } const signer = await embeddedWallet.signer() - return this.getSmartWallet(params, signer) + + // If neither walletAddress nor deploymentOwners provided, default to embedded wallet as single owner + const finalDeploymentOwners = deploymentOwners || (walletAddress ? undefined : [embeddedWallet.address]) + + return this.getSmartWallet( { + signer, + ...params, + deploymentOwners: finalDeploymentOwners, + }) + } + + /** + * Get an existing embedded wallet + * @description Retrieves an embedded wallet by walletId. This is useful when you have an embedded wallet ID and + * want to access the associated embedded wallet functionality. + * @param params - Wallet retrieval parameters + * @param params.walletId - ID of the embedded wallet to retrieve + * @returns Promise resolving to the embedded wallet instance + */ + async getEmbeddedWallet(params: GetEmbeddedWalletOptions) { + const { walletId } = params + return this.embeddedWalletProvider.getWallet({ + walletId, + }) } - private async getSmartWallet(params: GetWalletOptions, signer: LocalAccount) { + /** + * Get an existing smart wallet with a provided signer + * @description Retrieves a smart wallet using a directly provided signer. This is useful when + * you already have a LocalAccount signer and want to access an existing smart wallet without + * going through the embedded wallet provider. Use this instead of getSmartWalletWithEmbeddedSigner + * when you have direct control over the signer. + * @param signer - Local account to use for signing transactions on the smart wallet + * @param getWalletParams - Wallet retrieval parameters + * @param getWalletParams.deploymentOwners - Array of original deployment owners for smart wallet address calculation. Required if walletAddress not provided. Must match the exact owners array used during wallet deployment. + * @param getWalletParams.signerOwnerIndex - Current index of the signer in the smart wallet's current owners array (used for transaction signing). Defaults to 0 if not specified. This may differ from the original deployment index if owners have been modified. + * @param getWalletParams.walletAddress - Optional explicit smart wallet address (skips address calculation) + * @param getWalletParams.nonce - Optional nonce used during smart wallet creation + * @returns Promise resolving to the smart wallet instance with the provided signer + * @throws Error if neither walletAddress nor deploymentOwners provided + */ + async getSmartWallet(params: GetSmartWalletOptions) { const { - owners, + signer, + deploymentOwners, signerOwnerIndex, walletAddress: walletAddressParam, nonce, } = params + + if (!walletAddressParam && !deploymentOwners) { + try { + throw new Error( + 'Either walletAddress or deploymentOwners array must be provided to locate the smart wallet', + ) + } catch (error) { + console.error(error) + throw new Error( + 'Either walletAddress or deploymentOwners array must be provided to locate the smart wallet', + ) + } +} + + const ownerIndex = signerOwnerIndex ?? 0 + const walletAddress = walletAddressParam || (await this.smartWalletProvider.getWalletAddress({ - owners: owners || [signer.address], + // Safe to use ! since we validated above + owners: deploymentOwners!, nonce, })) return this.smartWalletProvider.getWallet({ walletAddress, signer, - ownerIndex: signerOwnerIndex, + ownerIndex, }) } } diff --git a/packages/sdk/src/wallet/base/SmartWallet.ts b/packages/sdk/src/wallet/base/SmartWallet.ts index 16daf89e..9f41b2f7 100644 --- a/packages/sdk/src/wallet/base/SmartWallet.ts +++ b/packages/sdk/src/wallet/base/SmartWallet.ts @@ -13,6 +13,8 @@ export abstract class SmartWallet { abstract signer: LocalAccount abstract getAddress(): Promise
abstract getBalance(): Promise + // TODO: add addSigner method + // TODO: add removeSigner method abstract send( transactionData: TransactionData, chainId: SupportedChainId, diff --git a/packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts b/packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts index 898b629c..5fae17dd 100644 --- a/packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts +++ b/packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts @@ -8,18 +8,47 @@ import type { SmartWallet } from '@/wallet/base/SmartWallet.js' * @description Abstract interface for smart wallet providers (Native, etc.) */ export abstract class SmartWalletProvider { + /** + * Create a new smart wallet instance + * @description Creates a new smart wallet that will be deployed on first transaction. + * The wallet address is deterministically calculated from owners and nonce. + * @param params - Wallet creation parameters + * @param params.owners - Array of wallet owners (addresses or WebAuthn public keys) + * @param params.signer - Local account used for signing transactions + * @param params.nonce - Optional nonce for address generation (defaults to 0) + * @returns Promise resolving to a new SmartWallet instance + */ abstract createWallet(params: { owners: Array
signer: LocalAccount nonce?: bigint }): Promise + /** + * Get an existing smart wallet instance + * @description Creates a SmartWallet instance for an already deployed wallet. + * Use this when you know the wallet address and want to interact with it. + * @param params - Wallet retrieval parameters + * @param params.walletAddress - Address of the deployed smart wallet + * @param params.signer - Local account used for signing transactions + * @param params.ownerIndex - Index of the signer in the wallet's owner list (defaults to 0) + * @returns SmartWallet instance for the existing wallet + */ abstract getWallet(params: { walletAddress: Address signer: LocalAccount ownerIndex?: number }): SmartWallet + /** + * Get the predicted smart wallet address + * @description Calculates the deterministic address where a smart wallet would be deployed + * given the specified owners and nonce. Uses CREATE2 for address prediction. + * @param params - Address prediction parameters + * @param params.owners - Array of wallet owners (addresses or WebAuthn public keys) + * @param params.nonce - Nonce for address generation (defaults to 0) + * @returns Promise resolving to the predicted wallet address + */ abstract getWalletAddress(params: { owners: Array
nonce?: bigint diff --git a/packages/sdk/src/wallet/providers/privy.ts b/packages/sdk/src/wallet/providers/privy.ts index 13daf1f4..71097722 100644 --- a/packages/sdk/src/wallet/providers/privy.ts +++ b/packages/sdk/src/wallet/providers/privy.ts @@ -1,7 +1,6 @@ -import { PrivyClient } from '@privy-io/server-auth' +import type { PrivyClient } from '@privy-io/server-auth' import { getAddress } from 'viem' -import type { ChainManager } from '@/services/ChainManager.js' import { PrivyWallet } from '@/wallet/PrivyWallet.js' import { EmbeddedWalletProvider } from '@/wallet/providers/base/EmbeddedWalletProvider.js' @@ -22,18 +21,14 @@ export interface PrivyProviderGetAllWalletsOptions { */ export class PrivyEmbeddedWalletProvider extends EmbeddedWalletProvider { public privy: PrivyClient - private chainManager: ChainManager /** * Create a new Privy wallet provider - * @param appId - Privy application ID - * @param appSecret - Privy application secret - * @param verbs - Verbs instance for accessing configured providers + * @param privyClient - Privy client instance */ - constructor(appId: string, appSecret: string, chainManager: ChainManager) { + constructor(privyClient: PrivyClient) { super() - this.privy = new PrivyClient(appId, appSecret) - this.chainManager = chainManager + this.privy = privyClient } /** @@ -49,8 +44,7 @@ export class PrivyEmbeddedWalletProvider extends EmbeddedWalletProvider { }) const walletInstance = new PrivyWallet( - this, - this.chainManager, + this.privy, wallet.id, getAddress(wallet.address), ) @@ -73,8 +67,7 @@ export class PrivyEmbeddedWalletProvider extends EmbeddedWalletProvider { }) const walletInstance = new PrivyWallet( - this, - this.chainManager, + this.privy, wallet.id, getAddress(wallet.address), ) @@ -101,8 +94,7 @@ export class PrivyEmbeddedWalletProvider extends EmbeddedWalletProvider { return response.data.map((wallet) => { const walletInstance = new PrivyWallet( - this, - this.chainManager, + this.privy, wallet.id, getAddress(wallet.address), ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b2f8d7a..b930a3c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,8 +225,8 @@ importers: specifier: ^2.4.1 version: 2.4.1 '@privy-io/server-auth': - specifier: latest - version: 1.28.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.0.5)) + specifier: '>=1.28.0' + version: 1.31.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.0.5)) viem: specifier: ^2.24.1 version: 2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.0.5) @@ -1556,30 +1556,12 @@ packages: resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} engines: {node: '>=12'} - '@privy-io/api-base@1.5.2': - resolution: {integrity: sha512-0eJBoQNmCSsWSWhzEVSU8WqPm7bgeN6VaAmqeXvjk8Ni0jM8nyTYjmRAqiCSs3mRzsnlQVchkGR6lsMTHkHKbw==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - '@privy-io/api-base@1.6.0': resolution: {integrity: sha512-ftlqjFw0Ww7Xn6Ad/1kEUsXRfKqNdmJYKat4ryJl2uPh60QXXlPfnf4y17dDFHJlnVb7qY10cCvKVz5ev5gAeg==} - '@privy-io/public-api@2.39.2': - resolution: {integrity: sha512-olT2xyrVdmgcxxy4g5v/u1qm6poDz3VNRuUcu45aiCP0DfAiseFVxKOKHpXP6xpA1m3z+45HL6lv2mJuYNP67w==} - '@privy-io/public-api@2.43.1': resolution: {integrity: sha512-zhGBTghZiwnqdA4YvrXXM7fsz3fWUltSkxNdnQTqKGb/IfV8aZ14ryuWvD4v5oPJGtqVcwKRfdDmW8TMPGZHog==} - '@privy-io/server-auth@1.28.6': - resolution: {integrity: sha512-Czx6LTMj81FymrSrrkoTaC5QEJhlBp94sWCyIBCS0SpQd76C7Y7KQffcMB+g70xjVGsD187Qc5iskHp6GfMJ7g==} - peerDependencies: - ethers: ^6 - viem: ^2.24.1 - peerDependenciesMeta: - ethers: - optional: true - viem: - optional: true - '@privy-io/server-auth@1.31.1': resolution: {integrity: sha512-w0DT0VZCPcXa/Mxqzo7fhXoInX5i4J5BgvzjNsdtMuovgR790kMx/9+K/rSlgtQ/25/B7oDjoIk/f8kd5Ps6mA==} peerDependencies: @@ -6032,26 +6014,10 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 - '@privy-io/api-base@1.5.2': - dependencies: - zod: 3.25.76 - '@privy-io/api-base@1.6.0': dependencies: zod: 3.25.76 - '@privy-io/public-api@2.39.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': - dependencies: - '@privy-io/api-base': 1.5.2 - bs58: 5.0.0 - libphonenumber-js: 1.12.10 - viem: 2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - bufferutil - - typescript - - utf-8-validate - '@privy-io/public-api@2.43.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: '@privy-io/api-base': 1.6.0 @@ -6064,30 +6030,6 @@ snapshots: - typescript - utf-8-validate - '@privy-io/server-auth@1.28.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.0.5))': - dependencies: - '@hpke/chacha20poly1305': 1.6.3 - '@hpke/core': 1.7.3 - '@noble/curves': 1.9.4 - '@noble/hashes': 1.8.0 - '@privy-io/public-api': 2.39.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - canonicalize: 2.1.0 - dotenv: 16.6.1 - jose: 4.15.9 - node-fetch-native: 1.6.6 - redaxios: 0.5.1 - svix: 1.69.0 - ts-case-convert: 2.1.0 - type-fest: 3.13.1 - optionalDependencies: - viem: 2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.0.5) - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - '@privy-io/server-auth@1.31.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.33.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.0.5))': dependencies: '@hpke/chacha20poly1305': 1.6.3 From 7ccfdec0aec61ca6c4d9ebfb91a029a1260078b7 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 16:02:47 -0700 Subject: [PATCH 20/39] clean up --- .../sdk/src/lend/providers/morpho/vaults.ts | 38 +++++------ packages/sdk/src/services/ChainManager.ts | 2 + packages/sdk/src/verbs.ts | 67 ++++++++++++------- packages/sdk/src/wallet/WalletNamespace.ts | 2 +- packages/sdk/src/wallet/WalletProvider.ts | 33 ++++----- 5 files changed, 82 insertions(+), 60 deletions(-) diff --git a/packages/sdk/src/lend/providers/morpho/vaults.ts b/packages/sdk/src/lend/providers/morpho/vaults.ts index eaf0fbdd..89a1e080 100644 --- a/packages/sdk/src/lend/providers/morpho/vaults.ts +++ b/packages/sdk/src/lend/providers/morpho/vaults.ts @@ -1,14 +1,16 @@ import type { AccrualPosition, IToken } from '@morpho-org/blue-sdk' import { fetchAccrualVault } from '@morpho-org/blue-sdk-viem' import type { Address } from 'viem' -import { baseSepolia } from 'viem/chains' +import { baseSepolia, unichain } from 'viem/chains' import type { SupportedChainId } from '@/constants/supportedChains.js' +import { + fetchRewards, + type RewardsBreakdown, +} from '@/lend/providers/morpho/api.js' import type { ChainManager } from '@/services/ChainManager.js' - -import { getTokenAddress, SUPPORTED_TOKENS } from '../../../supported/tokens.js' -import type { ApyBreakdown, LendVaultInfo } from '../../../types/lend.js' -import { fetchRewards, type RewardsBreakdown } from './api.js' +import { getTokenAddress, SUPPORTED_TOKENS } from '@/supported/tokens.js' +import type { ApyBreakdown, LendVaultInfo } from '@/types/lend.js' /** * Vault configuration type @@ -24,18 +26,18 @@ export interface VaultConfig { * Supported vaults on Unichain for Morpho lending */ export const SUPPORTED_VAULTS: VaultConfig[] = [ - // { - // // Gauntlet USDC vault - primary supported vault - // address: '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' as Address, - // chainId: unichain.id, - // name: 'Gauntlet USDC', - // 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, + chainId: unichain.id, + name: 'Gauntlet USDC', + asset: { + address: getTokenAddress('USDC', 130)!, // USDC on Unichain + symbol: SUPPORTED_TOKENS.USDC.symbol, + decimals: BigInt(SUPPORTED_TOKENS.USDC.decimals), + name: SUPPORTED_TOKENS.USDC.name, + }, + }, { address: '0x99067e5D73b1d6F1b5856E59209e12F5a0f86DED', chainId: baseSepolia.id, @@ -134,14 +136,12 @@ export async function getVaultInfo( chainManager: ChainManager, ): Promise { try { - console.log('Getting vault info for address', vaultAddress) // 1. Find vault configuration for validation const config = SUPPORTED_VAULTS.find((c) => c.address === vaultAddress) if (!config) { throw new Error(`Vault ${vaultAddress} not found`) } - console.log('Vault config found', config) // 2. Fetch live vault data from Morpho SDK const vault = await fetchAccrualVault( diff --git a/packages/sdk/src/services/ChainManager.ts b/packages/sdk/src/services/ChainManager.ts index 0ac0a7d8..3657f230 100644 --- a/packages/sdk/src/services/ChainManager.ts +++ b/packages/sdk/src/services/ChainManager.ts @@ -6,6 +6,8 @@ import { createBundlerClient } from 'viem/account-abstraction' import type { SUPPORTED_CHAIN_IDS } from '@/constants/supportedChains.js' import type { ChainConfig } from '@/types/chain.js' +// TODO: Add better documentation for this class once design is finalized + /** * Chain Manager Service * @description Manages public clients and chain infrastructure for the Verbs SDK diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index 93716c20..06c21fd7 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -20,8 +20,8 @@ export class Verbs { public readonly wallet: WalletNamespace private _chainManager: ChainManager private lendProvider?: LendProvider - private embeddedWalletProvider: EmbeddedWalletProvider - private smartWalletProvider: SmartWalletProvider + private embeddedWalletProvider!: EmbeddedWalletProvider + private smartWalletProvider!: SmartWalletProvider constructor(config: VerbsConfig) { this._chainManager = new ChainManager( @@ -47,19 +47,47 @@ export class Verbs { } } - if (config.wallet.embeddedWalletConfig.provider.type === 'privy') { + this.wallet = this.createWalletNamespace(config.wallet) + } + + /** + * Get the lend provider instance + * @returns LendProvider instance if configured, undefined otherwise + */ + get lend(): LendProvider { + if (!this.lendProvider) { + throw new Error('Lend provider not configured') + } + return this.lendProvider + } + + /** + * Get the chain manager instance + * @returns ChainManager instance for multi-chain operations + */ + get chainManager(): ChainManager { + return this._chainManager + } + + /** + * Create the wallet provider instance + * @param config - Wallet configuration + * @returns WalletProvider instance + */ + private createWalletProvider(config: VerbsConfig['wallet']) { + if (config.embeddedWalletConfig.provider.type === 'privy') { this.embeddedWalletProvider = new PrivyEmbeddedWalletProvider( - config.wallet.embeddedWalletConfig.provider.privyClient, + config.embeddedWalletConfig.provider.privyClient, ) } else { throw new Error( - `Unsupported embedded wallet provider: ${config.wallet.embeddedWalletConfig.provider.type}`, + `Unsupported embedded wallet provider: ${config.embeddedWalletConfig.provider.type}`, ) } if ( - !config.wallet.smartWalletConfig || - config.wallet.smartWalletConfig.provider.type === 'default' + !config.smartWalletConfig || + config.smartWalletConfig.provider.type === 'default' ) { this.smartWalletProvider = new DefaultSmartWalletProvider( this.chainManager, @@ -67,34 +95,25 @@ export class Verbs { ) } else { throw new Error( - `Unsupported smart wallet provider: ${config.wallet.smartWalletConfig.provider.type}`, + `Unsupported smart wallet provider: ${config.smartWalletConfig.provider.type}`, ) } - // Create unified wallet provider const walletProvider = new WalletProvider( this.embeddedWalletProvider, this.smartWalletProvider, ) - this.wallet = new WalletNamespace(walletProvider) - } - /** - * Get the lend provider instance - * @returns LendProvider instance if configured, undefined otherwise - */ - get lend(): LendProvider { - if (!this.lendProvider) { - throw new Error('Lend provider not configured') - } - return this.lendProvider + return walletProvider } /** - * Get the chain manager instance - * @returns ChainManager instance for multi-chain operations + * Create the wallet namespace instance + * @param config - Wallet configuration + * @returns WalletNamespace instance */ - get chainManager(): ChainManager { - return this._chainManager + private createWalletNamespace(config: VerbsConfig['wallet']) { + const walletProvider = this.createWalletProvider(config) + return new WalletNamespace(walletProvider) } } diff --git a/packages/sdk/src/wallet/WalletNamespace.ts b/packages/sdk/src/wallet/WalletNamespace.ts index 296cc6cc..6fce3bdb 100644 --- a/packages/sdk/src/wallet/WalletNamespace.ts +++ b/packages/sdk/src/wallet/WalletNamespace.ts @@ -86,7 +86,7 @@ export class WalletNamespace { * Get an existing smart wallet using embedded wallet as signer * @description Retrieves an embedded wallet by walletId and uses it as the signer to get * the corresponding smart wallet. If neither walletAddress nor deploymentOwners is provided, - * defaults to using the embedded wallet as the single owner. This is useful when you have + * defaults to using the embedded wallet as the single owner. This is useful when you have * an embedded wallet ID and want to access the associated smart wallet functionality. * @param params - Wallet retrieval parameters * @param params.walletId - ID of the embedded wallet to use as signer diff --git a/packages/sdk/src/wallet/WalletProvider.ts b/packages/sdk/src/wallet/WalletProvider.ts index 5c3d78e1..be9765cc 100644 --- a/packages/sdk/src/wallet/WalletProvider.ts +++ b/packages/sdk/src/wallet/WalletProvider.ts @@ -98,7 +98,7 @@ export class WalletProvider { /** * Get an existing smart wallet using embedded wallet as signer * @description Retrieves an embedded wallet by walletId and uses it as the signer to get - * the corresponding smart wallet. This is useful when you have + * the corresponding smart wallet. This is useful when you have * an embedded wallet ID and want to access the associated smart wallet functionality. * @dev If neither walletAddress nor deploymentOwners is provided, * defaults to using the embedded wallet as the single owner. @@ -122,12 +122,13 @@ export class WalletProvider { throw new Error('Embedded wallet not found') } const signer = await embeddedWallet.signer() - + // If neither walletAddress nor deploymentOwners provided, default to embedded wallet as single owner - const finalDeploymentOwners = deploymentOwners || (walletAddress ? undefined : [embeddedWallet.address]) - - return this.getSmartWallet( { - signer, + const finalDeploymentOwners = + deploymentOwners || (walletAddress ? undefined : [embeddedWallet.address]) + + return this.getSmartWallet({ + signer, ...params, deploymentOwners: finalDeploymentOwners, }) @@ -173,17 +174,17 @@ export class WalletProvider { } = params if (!walletAddressParam && !deploymentOwners) { - try { - throw new Error( - 'Either walletAddress or deploymentOwners array must be provided to locate the smart wallet', - ) - } catch (error) { - console.error(error) - throw new Error( - 'Either walletAddress or deploymentOwners array must be provided to locate the smart wallet', - ) + try { + throw new Error( + 'Either walletAddress or deploymentOwners array must be provided to locate the smart wallet', + ) + } catch (error) { + console.error(error) + throw new Error( + 'Either walletAddress or deploymentOwners array must be provided to locate the smart wallet', + ) + } } -} const ownerIndex = signerOwnerIndex ?? 0 From 4fc6d97146022ab064883606cbdd35e9700efc6e Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 16:10:33 -0700 Subject: [PATCH 21/39] fix serialization bug --- packages/demo/backend/src/controllers/lend.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/demo/backend/src/controllers/lend.ts b/packages/demo/backend/src/controllers/lend.ts index f19afce0..9f1d8565 100644 --- a/packages/demo/backend/src/controllers/lend.ts +++ b/packages/demo/backend/src/controllers/lend.ts @@ -5,6 +5,7 @@ import { z } from 'zod' import { validateRequest } from '../helpers/validation.js' import * as lendService from '../services/lend.js' +import { serializeBigInt } from '../utils/serializers.js' const DepositRequestSchema = z.object({ body: z.object({ @@ -133,13 +134,13 @@ export class LendController { return c.json({ transaction: { hash: result.hash, - // amount: result.amount.toString(), - // asset: result.asset, - // marketId: result.marketId, - // apy: result.apy, - // timestamp: result.timestamp, - // slippage: result.slippage, - // transactionData: result.transactionData, + amount: result.amount.toString(), + asset: result.asset, + marketId: result.marketId, + apy: result.apy, + timestamp: result.timestamp, + slippage: result.slippage, + transactionData: serializeBigInt(result.transactionData), }, }) } catch (error) { From 3e39786c9a16e718173d8c117b77656c1c377003 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 16:18:12 -0700 Subject: [PATCH 22/39] nit --- packages/sdk/src/wallet/PrivyWallet.ts | 4 ++-- packages/sdk/src/wallet/SmartWallet.ts | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/sdk/src/wallet/PrivyWallet.ts b/packages/sdk/src/wallet/PrivyWallet.ts index c08f2aaa..854a78b9 100644 --- a/packages/sdk/src/wallet/PrivyWallet.ts +++ b/packages/sdk/src/wallet/PrivyWallet.ts @@ -56,11 +56,11 @@ export class PrivyWallet extends EmbeddedWallet { return signed.signature as Hash }, async signTransaction() { - // Implement if needed + // TODO: Implement throw new Error('Not implemented') }, async signTypedData() { - // Implement if needed + // TODO: Implement throw new Error('Not implemented') }, }) diff --git a/packages/sdk/src/wallet/SmartWallet.ts b/packages/sdk/src/wallet/SmartWallet.ts index 66481e38..7d4f6493 100644 --- a/packages/sdk/src/wallet/SmartWallet.ts +++ b/packages/sdk/src/wallet/SmartWallet.ts @@ -76,6 +76,12 @@ export class DefaultSmartWallet extends SmartWallet { this.nonce = nonce } + /** + * Get the signer account for this smart wallet + * @description Returns the LocalAccount instance used for signing transactions and UserOperations. + * This signer is used to authorize operations on behalf of the smart wallet. + * @returns The LocalAccount signer configured for this smart wallet + */ get signer(): LocalAccount { return this._signer } @@ -162,7 +168,6 @@ export class DefaultSmartWallet extends SmartWallet { options?: LendOptions, ): Promise { // Parse human-readable inputs - // TODO: Get actual chain ID from wallet context, for now using Unichain const { amount: parsedAmount, asset: resolvedAsset } = parseLendParams( amount, asset, @@ -208,14 +213,10 @@ export class DefaultSmartWallet extends SmartWallet { calls, paymaster: true, }) - const receipt = await bundlerClient.waitForUserOperationReceipt({ + await bundlerClient.waitForUserOperationReceipt({ hash, }) - console.log('✅ Transaction successfully sponsored!') - console.log( - `⛽ View sponsored UserOperation on blockscout: https://base-sepolia.blockscout.com/op/${receipt.userOpHash}`, - ) return hash } catch (error) { throw new Error( From ca4dcd16570a1df1c9947daec83f0d31a167c105 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 16:26:30 -0700 Subject: [PATCH 23/39] all files are now well documented --- .../sdk/src/wallet/base/EmbeddedWallet.ts | 16 ++++++ packages/sdk/src/wallet/base/SmartWallet.ts | 50 +++++++++++++++++++ .../providers/base/EmbeddedWalletProvider.ts | 22 +++++++- packages/sdk/src/wallet/providers/privy.ts | 2 + 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/wallet/base/EmbeddedWallet.ts b/packages/sdk/src/wallet/base/EmbeddedWallet.ts index 3a20fdb4..5ed865c5 100644 --- a/packages/sdk/src/wallet/base/EmbeddedWallet.ts +++ b/packages/sdk/src/wallet/base/EmbeddedWallet.ts @@ -1,11 +1,27 @@ import type { Address, LocalAccount } from 'viem' +/** + * Base embedded wallet class + * @description Abstract base class for embedded wallet implementations (Privy, Dynamic, etc.). + * Provides a standard interface for embedded wallets that can be used as signers for smart wallets. + */ export abstract class EmbeddedWallet { + /** The wallet's Ethereum address */ public readonly address: Address + /** + * Create an embedded wallet instance + * @param address - The wallet's Ethereum address + */ constructor(address: Address) { this.address = address } + /** + * Get a signer for this embedded wallet + * @description Returns a LocalAccount that can be used to sign transactions and messages. + * This is typically used as the signer for smart wallet operations. + * @returns Promise resolving to a LocalAccount configured for signing operations + */ abstract signer(): Promise } diff --git a/packages/sdk/src/wallet/base/SmartWallet.ts b/packages/sdk/src/wallet/base/SmartWallet.ts index 9f41b2f7..2760bd8a 100644 --- a/packages/sdk/src/wallet/base/SmartWallet.ts +++ b/packages/sdk/src/wallet/base/SmartWallet.ts @@ -9,16 +9,56 @@ import type { import type { TokenBalance } from '@/types/token.js' import type { AssetIdentifier } from '@/utils/assets.js' +/** + * Base smart wallet class + * @description Abstract base class for smart wallet implementations (ERC-4337 compatible wallets). + */ export abstract class SmartWallet { + /** The LocalAccount used for signing transactions on behalf of this smart wallet */ abstract signer: LocalAccount + + /** + * Get the smart wallet's address + * @description Returns the deployed or predicted address of this smart wallet contract. + * For undeployed wallets, this returns the deterministic CREATE2 address. + * @returns Promise resolving to the wallet's Ethereum address + */ abstract getAddress(): Promise
+ + /** + * Get all token balances for this wallet + * @description Retrieves balances for all supported tokens held by this smart wallet. + * @returns Promise resolving to an array of token balances with amounts and metadata + */ abstract getBalance(): Promise + // TODO: add addSigner method // TODO: add removeSigner method + + /** + * Send a transaction using this smart wallet + * @description Executes a transaction through the smart wallet, handling gas sponsorship + * and ERC-4337 UserOperation creation automatically. + * @param transactionData - The transaction data to execute + * @param chainId - Target blockchain chain ID + * @returns Promise resolving to the transaction hash + */ abstract send( transactionData: TransactionData, chainId: SupportedChainId, ): Promise + + /** + * Lend tokens to a lending protocol + * @description Deposits tokens into a lending market to earn yield. + * Handles token approvals, market selection, and transaction execution. + * @param amount - Amount to lend in human-readable format + * @param asset - Asset identifier for the token to lend + * @param chainId - Target blockchain chain ID + * @param marketId - Optional specific market ID (auto-selected if not provided) + * @param options - Optional lending configuration (slippage, etc.) + * @returns Promise resolving to lending transaction details + */ abstract lend( amount: number, asset: AssetIdentifier, @@ -26,6 +66,16 @@ export abstract class SmartWallet { marketId?: string, options?: LendOptions, ): Promise + + /** + * Send tokens to another address + * @description Prepares transaction data for sending tokens from this smart wallet + * to a recipient address. Returns transaction data that can be executed via send(). + * @param amount - Amount to send in human-readable format + * @param asset - Asset identifier for the token to send + * @param recipientAddress - Destination address for the tokens + * @returns Promise resolving to prepared transaction data + */ abstract sendTokens( amount: number, asset: AssetIdentifier, diff --git a/packages/sdk/src/wallet/providers/base/EmbeddedWalletProvider.ts b/packages/sdk/src/wallet/providers/base/EmbeddedWalletProvider.ts index ce6d5e4d..e546bd8d 100644 --- a/packages/sdk/src/wallet/providers/base/EmbeddedWalletProvider.ts +++ b/packages/sdk/src/wallet/providers/base/EmbeddedWalletProvider.ts @@ -1,10 +1,28 @@ import type { EmbeddedWallet } from '@/wallet/base/EmbeddedWallet.js' /** - * Base embedded wallet provider interface - * @description Abstract interface for embedded wallet providers (Privy, Dynamic, etc.) + * Base embedded wallet provider class + * @description Abstract base class for embedded wallet provider implementations (Privy, Dynamic, etc.). + * Provides a standard interface for creating and retrieving embedded wallets that can be used + * as signers for smart wallets or standalone wallet functionality. */ export abstract class EmbeddedWalletProvider { + /** + * Create a new embedded wallet + * @description Creates a new embedded wallet instance using the provider's infrastructure. + * The wallet will be ready to use for signing transactions and messages. + * @returns Promise resolving to a new embedded wallet instance + */ abstract createWallet(): Promise + + /** + * Get an existing embedded wallet by ID + * @description Retrieves an existing embedded wallet using its unique identifier. + * The wallet must have been previously created through this provider. + * @param params - Wallet retrieval parameters + * @param params.walletId - Unique identifier for the embedded wallet + * @returns Promise resolving to the existing embedded wallet instance + * @throws Error if wallet with the specified ID is not found + */ abstract getWallet(params: { walletId: string }): Promise } diff --git a/packages/sdk/src/wallet/providers/privy.ts b/packages/sdk/src/wallet/providers/privy.ts index 71097722..1b6c8ab6 100644 --- a/packages/sdk/src/wallet/providers/privy.ts +++ b/packages/sdk/src/wallet/providers/privy.ts @@ -4,6 +4,8 @@ import { getAddress } from 'viem' import { PrivyWallet } from '@/wallet/PrivyWallet.js' import { EmbeddedWalletProvider } from '@/wallet/providers/base/EmbeddedWalletProvider.js' +// TODO: rename file to PrivyEmbeddedWalletProvider.ts + /** * Options for getting all wallets * @description Parameters for filtering and paginating wallet results From 0072d9e81766af965f3b723b8341dc7b77cacec7 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 16:28:54 -0700 Subject: [PATCH 24/39] nit --- packages/sdk/src/constants/supportedChains.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sdk/src/constants/supportedChains.ts b/packages/sdk/src/constants/supportedChains.ts index ceba057e..8ed1bb20 100644 --- a/packages/sdk/src/constants/supportedChains.ts +++ b/packages/sdk/src/constants/supportedChains.ts @@ -4,7 +4,6 @@ export const SUPPORTED_CHAIN_IDS = [ mainnet.id, unichain.id, base.id, - 901, baseSepolia.id, ] as const From cf2fc4f8c932c03aa41d00692ee0a3abaf907531 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 16:38:43 -0700 Subject: [PATCH 25/39] morpho tests working --- .../src/lend/providers/morpho/index.test.ts | 429 +++++++++--------- 1 file changed, 205 insertions(+), 224 deletions(-) diff --git a/packages/sdk/src/lend/providers/morpho/index.test.ts b/packages/sdk/src/lend/providers/morpho/index.test.ts index 9ca3c4d8..53add1cb 100644 --- a/packages/sdk/src/lend/providers/morpho/index.test.ts +++ b/packages/sdk/src/lend/providers/morpho/index.test.ts @@ -1,224 +1,205 @@ -// import { fetchAccrualVault } from '@morpho-org/blue-sdk-viem' -// import { type Address, createPublicClient, http, type PublicClient } from 'viem' -// import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -// import type { MorphoLendConfig } from '../../../types/lend.js' -// import { LendProviderMorpho } from './index.js' - -// // Mock chain config for Unichain -// const unichain = { -// id: 130, -// name: 'Unichain', -// nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, -// rpcUrls: { -// default: { http: ['https://rpc.unichain.org'] }, -// }, -// blockExplorers: { -// default: { -// name: 'Unichain Explorer', -// url: 'https://unichain.blockscout.com', -// }, -// }, -// } - -// // Mock the Morpho SDK modules -// vi.mock('@morpho-org/blue-sdk-viem', () => ({ -// fetchMarket: vi.fn(), -// fetchAccrualVault: vi.fn(), -// MetaMorphoAction: { -// deposit: vi.fn(() => '0x1234567890abcdef'), // Mock deposit function to return mock calldata -// }, -// })) - -// vi.mock('@morpho-org/morpho-ts', () => ({ -// Time: { -// timestamp: vi.fn(() => BigInt(Math.floor(Date.now() / 1000))), -// }, -// })) - -// vi.mock('@morpho-org/bundler-sdk-viem', () => ({ -// populateBundle: vi.fn(), -// finalizeBundle: vi.fn(), -// encodeBundle: vi.fn(), -// })) - -// describe('LendProviderMorpho', () => { -// let provider: LendProviderMorpho -// let mockConfig: MorphoLendConfig -// let mockPublicClient: ReturnType - -// beforeEach(() => { -// mockConfig = { -// type: 'morpho', -// defaultSlippage: 50, -// } - -// mockPublicClient = createPublicClient({ -// chain: unichain, -// transport: http(), -// }) - -// provider = new LendProviderMorpho( -// mockConfig, -// mockPublicClient as unknown as PublicClient, -// ) -// }) - -// describe('constructor', () => { -// it('should initialize with provided config', () => { -// expect(provider).toBeInstanceOf(LendProviderMorpho) -// }) - -// it('should use default slippage when not provided', () => { -// const configWithoutSlippage = { -// ...mockConfig, -// defaultSlippage: undefined, -// } -// const providerWithDefaults = new LendProviderMorpho( -// configWithoutSlippage, -// mockPublicClient as unknown as PublicClient, -// ) -// expect(providerWithDefaults).toBeInstanceOf(LendProviderMorpho) -// }) -// }) - -// describe('withdraw', () => { -// it('should throw error for unimplemented withdraw functionality', async () => { -// const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC -// const amount = BigInt('1000000000') // 1000 USDC -// const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault - -// await expect(provider.withdraw(asset, amount, marketId)).rejects.toThrow( -// 'Withdraw functionality not yet implemented', -// ) -// }) -// }) - -// describe('supportedNetworkIds', () => { -// it('should return array of supported network chain IDs', () => { -// const networkIds = provider.supportedNetworkIds() - -// expect(Array.isArray(networkIds)).toBe(true) -// expect(networkIds).toContain(130) // Unichain -// expect(networkIds.length).toBeGreaterThan(0) -// }) - -// it('should return unique network IDs', () => { -// const networkIds = provider.supportedNetworkIds() -// const uniqueIds = [...new Set(networkIds)] - -// expect(networkIds.length).toBe(uniqueIds.length) -// }) -// }) - -// describe('lend', () => { -// beforeEach(() => { -// // Mock vault data for all lend tests -// const mockVault = { -// totalAssets: BigInt(10000000e6), // 10M USDC -// totalSupply: BigInt(10000000e6), // 10M shares -// fee: BigInt(1e17), // 10% fee in WAD format -// owner: '0x5a4E19842e09000a582c20A4f524C26Fb48Dd4D0' as Address, -// curator: '0x9E33faAE38ff641094fa68c65c2cE600b3410585' as Address, -// allocations: new Map([ -// [ -// '0', -// { -// position: { -// supplyShares: BigInt(1000000e6), -// supplyAssets: BigInt(1000000e6), -// market: { -// supplyApy: BigInt(3e16), // 3% APY -// }, -// }, -// }, -// ], -// ]), -// } - -// vi.mocked(fetchAccrualVault).mockResolvedValue(mockVault as any) - -// // Mock the fetch API for rewards -// vi.stubGlobal( -// 'fetch', -// vi.fn().mockResolvedValue({ -// ok: true, -// json: async () => ({ -// data: { -// vaultByAddress: { -// state: { -// rewards: [], -// allocation: [], -// }, -// }, -// }, -// }), -// } as any), -// ) -// }) - -// afterEach(() => { -// vi.unstubAllGlobals() -// }) - -// it('should successfully create a lending transaction', async () => { -// const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC -// const amount = BigInt('1000000000') // 1000 USDC -// const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault - -// const lendTransaction = await provider.lend(asset, amount, marketId, { -// receiver: '0x1234567890123456789012345678901234567890' as Address, -// }) - -// expect(lendTransaction).toHaveProperty('amount', amount) -// expect(lendTransaction).toHaveProperty('asset', asset) -// expect(lendTransaction).toHaveProperty('marketId', marketId) -// expect(lendTransaction).toHaveProperty('apy') -// expect(lendTransaction).toHaveProperty('timestamp') -// expect(lendTransaction).toHaveProperty('transactionData') -// expect(lendTransaction.transactionData).toHaveProperty('approval') -// expect(lendTransaction.transactionData).toHaveProperty('deposit') -// expect(typeof lendTransaction.apy).toBe('number') -// expect(lendTransaction.apy).toBeGreaterThan(0) -// }) - -// it('should find best market when marketId not provided', async () => { -// const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC -// const amount = BigInt('1000000000') // 1000 USDC - -// // Mock the market data for getMarketInfo - -// const lendTransaction = await provider.lend(asset, amount, undefined, { -// receiver: '0x1234567890123456789012345678901234567890' as Address, -// }) - -// expect(lendTransaction).toHaveProperty('marketId') -// expect(lendTransaction.marketId).toBeTruthy() -// }) - -// it('should handle lending errors', async () => { -// const asset = '0x0000000000000000000000000000000000000000' as Address // Invalid asset -// const amount = BigInt('1000000000') - -// await expect(provider.lend(asset, amount)).rejects.toThrow( -// 'Failed to lend', -// ) -// }) - -// it('should use custom slippage when provided', async () => { -// const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address -// const amount = BigInt('1000000000') -// const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault -// const customSlippage = 100 // 1% - -// // Mock the market data for getMarketInfo - -// const lendTransaction = await provider.lend(asset, amount, marketId, { -// slippage: customSlippage, -// receiver: '0x1234567890123456789012345678901234567890' as Address, -// }) - -// expect(lendTransaction).toHaveProperty('amount', amount) -// }) -// }) -// }) +import { fetchAccrualVault } from '@morpho-org/blue-sdk-viem' +import { type Address } from 'viem' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import type { ChainManager } from '@/services/ChainManager.js' +import { MockChainManager } from '@/test/MockChainManager.js' + +import type { MorphoLendConfig } from '../../../types/lend.js' +import { LendProviderMorpho } from './index.js' + +// Mock the Morpho SDK modules +vi.mock('@morpho-org/blue-sdk-viem', () => ({ + fetchMarket: vi.fn(), + fetchAccrualVault: vi.fn(), + MetaMorphoAction: { + deposit: vi.fn(() => '0x1234567890abcdef'), // Mock deposit function to return mock calldata + }, +})) + +vi.mock('@morpho-org/morpho-ts', () => ({ + Time: { + timestamp: vi.fn(() => BigInt(Math.floor(Date.now() / 1000))), + }, +})) + +vi.mock('@morpho-org/bundler-sdk-viem', () => ({ + populateBundle: vi.fn(), + finalizeBundle: vi.fn(), + encodeBundle: vi.fn(), +})) + +describe('LendProviderMorpho', () => { + let provider: LendProviderMorpho + let mockConfig: MorphoLendConfig + let mockChainManager: ChainManager + + beforeEach(() => { + mockConfig = { + type: 'morpho', + defaultSlippage: 50, + } + + mockChainManager = new MockChainManager() as unknown as ChainManager + + provider = new LendProviderMorpho(mockConfig, mockChainManager) + }) + + describe('constructor', () => { + it('should initialize with provided config', () => { + expect(provider).toBeInstanceOf(LendProviderMorpho) + }) + + it('should use default slippage when not provided', () => { + const configWithoutSlippage = { + ...mockConfig, + defaultSlippage: undefined, + } + const providerWithDefaults = new LendProviderMorpho( + configWithoutSlippage, + mockChainManager, + ) + expect(providerWithDefaults).toBeInstanceOf(LendProviderMorpho) + }) + }) + + describe('withdraw', () => { + it('should throw error for unimplemented withdraw functionality', async () => { + const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC + const amount = BigInt('1000000000') // 1000 USDC + const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault + + await expect(provider.withdraw(asset, amount, marketId)).rejects.toThrow( + 'Withdraw functionality not yet implemented', + ) + }) + }) + + describe('supportedNetworkIds', () => { + it('should return array of supported network chain IDs', () => { + const networkIds = provider.supportedNetworkIds() + + expect(Array.isArray(networkIds)).toBe(true) + expect(networkIds).toContain(130) // Unichain + expect(networkIds.length).toBeGreaterThan(0) + }) + + it('should return unique network IDs', () => { + const networkIds = provider.supportedNetworkIds() + const uniqueIds = [...new Set(networkIds)] + + expect(networkIds.length).toBe(uniqueIds.length) + }) + }) + + describe('lend', () => { + beforeEach(() => { + // Mock vault data for all lend tests + const mockVault = { + totalAssets: BigInt(10000000e6), // 10M USDC + totalSupply: BigInt(10000000e6), // 10M shares + fee: BigInt(1e17), // 10% fee in WAD format + owner: '0x5a4E19842e09000a582c20A4f524C26Fb48Dd4D0' as Address, + curator: '0x9E33faAE38ff641094fa68c65c2cE600b3410585' as Address, + allocations: new Map([ + [ + '0', + { + position: { + supplyShares: BigInt(1000000e6), + supplyAssets: BigInt(1000000e6), + market: { + supplyApy: BigInt(3e16), // 3% APY + }, + }, + }, + ], + ]), + } + + vi.mocked(fetchAccrualVault).mockResolvedValue(mockVault as any) + + // Mock the fetch API for rewards + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + data: { + vaultByAddress: { + state: { + rewards: [], + allocation: [], + }, + }, + }, + }), + } as any), + ) + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('should successfully create a lending transaction', async () => { + const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC + const amount = BigInt('1000000000') // 1000 USDC + const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault + + const lendTransaction = await provider.lend(asset, amount, marketId, { + receiver: '0x1234567890123456789012345678901234567890' as Address, + }) + + expect(lendTransaction).toHaveProperty('amount', amount) + expect(lendTransaction).toHaveProperty('asset', asset) + expect(lendTransaction).toHaveProperty('marketId', marketId) + expect(lendTransaction).toHaveProperty('apy') + expect(lendTransaction).toHaveProperty('timestamp') + expect(lendTransaction).toHaveProperty('transactionData') + expect(lendTransaction.transactionData).toHaveProperty('approval') + expect(lendTransaction.transactionData).toHaveProperty('deposit') + expect(typeof lendTransaction.apy).toBe('number') + expect(lendTransaction.apy).toBeGreaterThan(0) + }) + + it('should find best market when marketId not provided', async () => { + const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address // USDC + const amount = BigInt('1000000000') // 1000 USDC + + // Mock the market data for getMarketInfo + + const lendTransaction = await provider.lend(asset, amount, undefined, { + receiver: '0x1234567890123456789012345678901234567890' as Address, + }) + + expect(lendTransaction).toHaveProperty('marketId') + expect(lendTransaction.marketId).toBeTruthy() + }) + + it('should handle lending errors', async () => { + const asset = '0x0000000000000000000000000000000000000000' as Address // Invalid asset + const amount = BigInt('1000000000') + + await expect(provider.lend(asset, amount)).rejects.toThrow( + 'Failed to lend', + ) + }) + + it('should use custom slippage when provided', async () => { + const asset = '0x078d782b760474a361dda0af3839290b0ef57ad6' as Address + const amount = BigInt('1000000000') + const marketId = '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' // Gauntlet USDC vault + const customSlippage = 100 // 1% + + // Mock the market data for getMarketInfo + + const lendTransaction = await provider.lend(asset, amount, marketId, { + slippage: customSlippage, + receiver: '0x1234567890123456789012345678901234567890' as Address, + }) + + expect(lendTransaction).toHaveProperty('amount', amount) + }) + }) +}) From aba3137b13369c583410c2ace61423c9ebd60375 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 17:31:51 -0700 Subject: [PATCH 26/39] supersim tests passing --- .../providers/morpho/lend.supersim.test.ts | 554 +++++++++--------- packages/sdk/src/verbs.ts | 2 + packages/sdk/src/wallet/PrivyWallet.ts | 162 ++++- packages/sdk/src/wallet/providers/privy.ts | 19 +- 4 files changed, 463 insertions(+), 274 deletions(-) diff --git a/packages/sdk/src/lend/providers/morpho/lend.supersim.test.ts b/packages/sdk/src/lend/providers/morpho/lend.supersim.test.ts index b3d784a2..4fcf7f07 100644 --- a/packages/sdk/src/lend/providers/morpho/lend.supersim.test.ts +++ b/packages/sdk/src/lend/providers/morpho/lend.supersim.test.ts @@ -1,271 +1,283 @@ -// import type { ChildProcess } from 'child_process' -// import { config } from 'dotenv' -// import { type Address, erc20Abi, parseUnits, type PublicClient } from 'viem' -// import type { privateKeyToAccount } from 'viem/accounts' -// import { unichain } from 'viem/chains' -// import { afterAll, beforeAll, describe, expect, it } from 'vitest' - -// import type { VerbsInterface } from '../../../types/verbs.js' -// import type { Wallet } from '../../../types/wallet.js' -// import { -// ANVIL_ACCOUNTS, -// setupSupersimTest, -// stopSupersim, -// } from '../../../utils/test.js' -// import { initVerbs } from '../../../verbs.js' -// import { SUPPORTED_VAULTS } from './vaults.js' - -// // Load test environment variables -// config({ path: '.env.test.local' }) - -// // Use the first supported vault (Gauntlet USDC) -// const TEST_VAULT = SUPPORTED_VAULTS[0] -// const USDC_ADDRESS = TEST_VAULT.asset.address -// const TEST_VAULT_ADDRESS = TEST_VAULT.address -// const TEST_WALLET_ID = 'v6c9zr6cjoo91qlopwzo9nhl' -// const TEST_WALLET_ADDRESS = -// '0x55B05e38597D4365C59A6847f51849B30C381bA2' as Address - -// describe('Morpho Lend', () => { -// let supersimProcess: ChildProcess -// let publicClient: PublicClient -// let _testAccount: ReturnType -// let verbs: VerbsInterface -// let testWallet: Wallet | null - -// beforeAll(async () => { -// // Set up supersim with funded wallet using helper -// const setup = await setupSupersimTest({ -// supersim: { -// chains: ['unichain'], -// l1Port: 8546, -// l2StartingPort: 9546, -// }, -// wallet: { -// rpcUrl: 'http://127.0.0.1:9546', -// chain: unichain, -// amount: '10', -// fundUsdc: true, // Request USDC funding for vault testing -// usdcAmount: '1000', -// // Fund the Privy wallet address -// address: TEST_WALLET_ADDRESS, -// }, -// }) - -// supersimProcess = setup.supersimProcess -// publicClient = setup.publicClient -// _testAccount = setup.testAccount - -// // Initialize Verbs SDK with Morpho lending -// verbs = initVerbs({ -// wallet: { -// type: 'privy', -// appId: process.env.PRIVY_APP_ID || 'test-app-id', -// appSecret: process.env.PRIVY_APP_SECRET || 'test-app-secret', -// }, -// lend: { -// type: 'morpho', -// defaultSlippage: 50, -// }, -// chains: [ -// { -// chainId: unichain.id, -// rpcUrl: 'http://127.0.0.1:9546', -// }, -// ], -// }) - -// // Use Privy to get the wallet -// const wallet = await verbs.getWallet(TEST_WALLET_ID) - -// if (!wallet) { -// throw new Error(`Wallet ${TEST_WALLET_ID} not found in Privy`) -// } - -// testWallet = wallet - -// // Verify the address matches what we expect -// expect(testWallet!.address.toLowerCase()).toBe( -// TEST_WALLET_ADDRESS.toLowerCase(), -// ) -// }, 60000) - -// afterAll(async () => { -// await stopSupersim(supersimProcess) -// }) - -// it('should connect to forked Unichain', async () => { -// // Check that we can connect and get the chain ID -// const chainId = await publicClient.getChainId() -// expect(chainId).toBe(130) // Unichain chain ID - -// // Check that our Privy wallet has ETH -// const balance = await publicClient.getBalance({ -// address: TEST_WALLET_ADDRESS, -// }) -// expect(balance).toBeGreaterThan(0n) -// }) - -// it('should execute lend operation with real Morpho transactions', async () => { -// // First, verify the vault exists -// await verbs.lend.getVault(TEST_VAULT_ADDRESS) - -// // Check balances -// await publicClient.getBalance({ -// address: TEST_WALLET_ADDRESS, -// }) - -// // Check USDC balance -// try { -// await publicClient.readContract({ -// address: USDC_ADDRESS, -// abi: erc20Abi, -// functionName: 'balanceOf', -// args: [TEST_WALLET_ADDRESS], -// }) -// } catch { -// throw new Error('USDC balance not found') -// } - -// // Check vault balance before deposit -// const vaultBalanceBefore = await verbs.lend.getVaultBalance( -// TEST_VAULT_ADDRESS, -// TEST_WALLET_ADDRESS, -// ) - -// // Test the new human-readable API: lend(1, 'usdc') -// const lendTx = await testWallet!.lend(1, 'usdc', TEST_VAULT_ADDRESS, { -// slippage: 50, // 0.5% -// }) - -// const expectedAmount = parseUnits('1', 6) // 1 USDC (6 decimals) - -// // Validate lend transaction structure -// expect(lendTx).toBeDefined() -// expect(lendTx.amount).toBe(expectedAmount) -// expect(lendTx.asset).toBe(USDC_ADDRESS) -// expect(lendTx.marketId).toBe(TEST_VAULT_ADDRESS) -// expect(lendTx.apy).toBeGreaterThan(0) -// expect(lendTx.slippage).toBe(50) -// expect(lendTx.transactionData).toBeDefined() -// expect(lendTx.transactionData?.deposit).toBeDefined() -// expect(lendTx.transactionData?.approval).toBeDefined() - -// // Validate transaction data structure -// expect(lendTx.transactionData?.approval?.to).toBe(USDC_ADDRESS) -// expect(lendTx.transactionData?.approval?.data).toMatch(/^0x[0-9a-fA-F]+$/) -// expect(lendTx.transactionData?.approval?.value).toBe('0x0') - -// expect(lendTx.transactionData?.deposit?.to).toBe(TEST_VAULT_ADDRESS) -// expect(lendTx.transactionData?.deposit?.data).toMatch(/^0x[0-9a-fA-F]+$/) -// expect(lendTx.transactionData?.deposit?.value).toBe('0x0') - -// // Get the current nonce for the wallet -// await publicClient.getTransactionCount({ -// address: TEST_WALLET_ADDRESS, -// }) - -// // Test signing the approval transaction using wallet.sign() -// try { -// const approvalTx = lendTx.transactionData!.approval! - -// // First, estimate gas for approval transaction on supersim -// await publicClient.estimateGas({ -// account: TEST_WALLET_ADDRESS, -// to: approvalTx.to as `0x${string}`, -// data: approvalTx.data as `0x${string}`, -// value: BigInt(approvalTx.value), -// }) - -// const signedApproval = await testWallet!.sign(approvalTx) -// expect(signedApproval).toBeDefined() - -// // Send the signed transaction to supersim -// const approvalTxHash = await testWallet!.send( -// signedApproval, -// publicClient, -// ) -// expect(approvalTxHash).toMatch(/^0x[0-9a-fA-F]{64}$/) // Valid tx hash format - -// // Wait for approval to be mined -// await publicClient.waitForTransactionReceipt({ hash: approvalTxHash }) -// } catch { -// // This is expected if Privy wallet doesn't have gas on the right network -// } - -// // Test deposit transaction structure -// const depositTx = lendTx.transactionData!.deposit! - -// expect(depositTx.to).toBe(TEST_VAULT_ADDRESS) -// expect(depositTx.data.length).toBeGreaterThan(10) // Should have encoded function data -// expect(depositTx.data.startsWith('0x')).toBe(true) - -// // The deposit call data should include the deposit function selector -// // deposit(uint256,address) has selector 0x6e553f65 -// expect(depositTx.data.startsWith('0x6e553f65')).toBe(true) - -// // Test signing the deposit transaction using wallet.sign() -// try { -// const signedDeposit = await testWallet!.sign(depositTx) -// expect(signedDeposit).toBeDefined() - -// // Send the signed transaction to supersim -// const depositTxHash = await testWallet!.send(signedDeposit, publicClient) -// expect(depositTxHash).toMatch(/^0x[0-9a-fA-F]{64}$/) // Valid tx hash format - -// // Wait for deposit to be mined -// await publicClient.waitForTransactionReceipt({ hash: depositTxHash }) -// } catch { -// // This is expected if Privy wallet doesn't have gas on the right network -// } - -// // Check vault balance after deposit attempts -// const vaultBalanceAfter = await verbs.lend.getVaultBalance( -// TEST_VAULT_ADDRESS, -// TEST_WALLET_ADDRESS, -// ) - -// // For now, we expect the test to fail at signing since Privy needs proper setup -// // In production, the balance would increase after successful deposits -// expect(vaultBalanceBefore).toBeDefined() -// expect(vaultBalanceAfter).toBeDefined() -// }, 60000) - -// it('should handle different human-readable amounts', async () => { -// // Test fractional amounts -// const tx1 = await testWallet!.lend(0.5, 'usdc', TEST_VAULT_ADDRESS) -// const expectedAmount1 = parseUnits('0.5', 6) // 0.5 USDC -// expect(tx1.amount).toBe(expectedAmount1) - -// // Test large amounts -// const tx2 = await testWallet!.lend(1000, 'usdc', TEST_VAULT_ADDRESS) -// const expectedAmount2 = parseUnits('1000', 6) // 1000 USDC -// expect(tx2.amount).toBe(expectedAmount2) - -// // Test using address instead of symbol -// const tx3 = await testWallet!.lend(1, USDC_ADDRESS, TEST_VAULT_ADDRESS) -// const expectedAmount3 = parseUnits('1', 6) // 1 USDC -// expect(tx3.amount).toBe(expectedAmount3) -// expect(tx3.asset).toBe(USDC_ADDRESS) -// }, 30000) - -// it('should validate input parameters', async () => { -// // Test invalid amount -// await expect(testWallet!.lend(0, 'usdc')).rejects.toThrow( -// 'Amount must be greater than 0', -// ) -// await expect(testWallet!.lend(-1, 'usdc')).rejects.toThrow( -// 'Amount must be greater than 0', -// ) - -// // Test invalid asset symbol -// await expect(testWallet!.lend(1, 'invalid')).rejects.toThrow( -// 'Unsupported asset symbol: invalid', -// ) - -// // Test invalid address format -// await expect(testWallet!.lend(1, 'not-an-address')).rejects.toThrow( -// 'Unsupported asset symbol', -// ) -// }, 30000) -// }) +// load env variables +import 'dotenv/config' + +import { PrivyClient } from '@privy-io/server-auth' +import type { ChildProcess } from 'child_process' +import { config } from 'dotenv' +import { type Address, erc20Abi, parseUnits, type PublicClient } from 'viem' +import type { privateKeyToAccount } from 'viem/accounts' +import { unichain } from 'viem/chains' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { Verbs } from '@/verbs.js' +import type { PrivyWallet } from '@/wallet/PrivyWallet.js' + +import { setupSupersimTest, stopSupersim } from '../../../utils/test.js' +import { SUPPORTED_VAULTS } from './vaults.js' + +// Load test environment variables +config({ path: '.env.test.local' }) + +// Use the first supported vault (Gauntlet USDC) +const TEST_VAULT = SUPPORTED_VAULTS[0] +const USDC_ADDRESS = TEST_VAULT.asset.address +const TEST_VAULT_ADDRESS = TEST_VAULT.address +const TEST_WALLET_ID = 'v6c9zr6cjoo91qlopwzo9nhl' +const TEST_WALLET_ADDRESS = + '0x55B05e38597D4365C59A6847f51849B30C381bA2' as Address + +describe('Morpho Lend', () => { + let supersimProcess: ChildProcess + let publicClient: PublicClient + let _testAccount: ReturnType + let verbs: Verbs + let testWallet: PrivyWallet | null + + beforeAll(async () => { + // Set up supersim with funded wallet using helper + const setup = await setupSupersimTest({ + supersim: { + chains: ['unichain'], + l1Port: 8546, + l2StartingPort: 9546, + }, + wallet: { + rpcUrl: 'http://127.0.0.1:9546', + chain: unichain, + amount: '10', + fundUsdc: true, // Request USDC funding for vault testing + usdcAmount: '1000', + // Fund the Privy wallet address + address: TEST_WALLET_ADDRESS, + }, + }) + + supersimProcess = setup.supersimProcess + publicClient = setup.publicClient + _testAccount = setup.testAccount + const privyClient = new PrivyClient( + process.env.PRIVY_APP_ID || 'test-app-id', + process.env.PRIVY_APP_SECRET || 'test-app-secret', + ) + + // Initialize Verbs SDK with Morpho lending + verbs = new Verbs({ + wallet: { + embeddedWalletConfig: { + provider: { + type: 'privy', + privyClient, + }, + }, + smartWalletConfig: { + provider: { + type: 'default', + }, + }, + }, + lend: { + type: 'morpho', + defaultSlippage: 50, + }, + chains: [ + { + chainId: unichain.id, + rpcUrl: 'http://127.0.0.1:9546', + }, + ], + }) + + // Use Privy to get the wallet + testWallet = (await verbs.wallet.getEmbeddedWallet({ + walletId: TEST_WALLET_ID, + })) as PrivyWallet + + if (!testWallet) { + throw new Error(`Wallet ${TEST_WALLET_ID} not found in Privy`) + } + + // Verify the address matches what we expect + expect(testWallet.address.toLowerCase()).toBe( + TEST_WALLET_ADDRESS.toLowerCase(), + ) + }, 60000) + + afterAll(async () => { + await stopSupersim(supersimProcess) + }) + + it('should connect to forked Unichain', async () => { + // Check that we can connect and get the chain ID + const chainId = await publicClient.getChainId() + expect(chainId).toBe(130) // Unichain chain ID + + // Check that our Privy wallet has ETH + const balance = await publicClient.getBalance({ + address: TEST_WALLET_ADDRESS, + }) + expect(balance).toBeGreaterThan(0n) + }) + + it('should execute lend operation with real Morpho transactions', async () => { + // First, verify the vault exists + await verbs.lend.getVault(TEST_VAULT_ADDRESS) + + // Check balances + await publicClient.getBalance({ + address: TEST_WALLET_ADDRESS, + }) + + // Check USDC balance + try { + await publicClient.readContract({ + address: USDC_ADDRESS, + abi: erc20Abi, + functionName: 'balanceOf', + args: [TEST_WALLET_ADDRESS], + }) + } catch { + throw new Error('USDC balance not found') + } + + // Check vault balance before deposit + const vaultBalanceBefore = await verbs.lend.getVaultBalance( + TEST_VAULT_ADDRESS, + TEST_WALLET_ADDRESS, + ) + + // Test the new human-readable API: lend(1, 'usdc') + const lendTx = await testWallet!.lend(1, 'usdc', TEST_VAULT_ADDRESS, { + slippage: 50, // 0.5% + }) + + const expectedAmount = parseUnits('1', 6) // 1 USDC (6 decimals) + + // Validate lend transaction structure + expect(lendTx).toBeDefined() + expect(lendTx.amount).toBe(expectedAmount) + expect(lendTx.asset).toBe(USDC_ADDRESS) + expect(lendTx.marketId).toBe(TEST_VAULT_ADDRESS) + expect(lendTx.apy).toBeGreaterThan(0) + expect(lendTx.slippage).toBe(50) + expect(lendTx.transactionData).toBeDefined() + expect(lendTx.transactionData?.deposit).toBeDefined() + expect(lendTx.transactionData?.approval).toBeDefined() + + // Validate transaction data structure + expect(lendTx.transactionData?.approval?.to).toBe(USDC_ADDRESS) + expect(lendTx.transactionData?.approval?.data).toMatch(/^0x[0-9a-fA-F]+$/) + expect(lendTx.transactionData?.approval?.value).toBe(0n) + + expect(lendTx.transactionData?.deposit?.to).toBe(TEST_VAULT_ADDRESS) + expect(lendTx.transactionData?.deposit?.data).toMatch(/^0x[0-9a-fA-F]+$/) + expect(lendTx.transactionData?.deposit?.value).toBe(0n) + + // Get the current nonce for the wallet + await publicClient.getTransactionCount({ + address: TEST_WALLET_ADDRESS, + }) + + // Test signing the approval transaction using wallet.sign() + try { + const approvalTx = lendTx.transactionData!.approval! + + // First, estimate gas for approval transaction on supersim + await publicClient.estimateGas({ + account: TEST_WALLET_ADDRESS, + to: approvalTx.to as `0x${string}`, + data: approvalTx.data as `0x${string}`, + value: BigInt(approvalTx.value), + }) + + const signedApproval = await testWallet!.sign(approvalTx) + expect(signedApproval).toBeDefined() + + // Send the signed transaction to supersim + const approvalTxHash = await testWallet!.send( + signedApproval, + publicClient, + ) + expect(approvalTxHash).toMatch(/^0x[0-9a-fA-F]{64}$/) // Valid tx hash format + + // Wait for approval to be mined + await publicClient.waitForTransactionReceipt({ hash: approvalTxHash }) + } catch { + // This is expected if Privy wallet doesn't have gas on the right network + } + + // Test deposit transaction structure + const depositTx = lendTx.transactionData!.deposit! + + expect(depositTx.to).toBe(TEST_VAULT_ADDRESS) + expect(depositTx.data.length).toBeGreaterThan(10) // Should have encoded function data + expect(depositTx.data.startsWith('0x')).toBe(true) + + // The deposit call data should include the deposit function selector + // deposit(uint256,address) has selector 0x6e553f65 + expect(depositTx.data.startsWith('0x6e553f65')).toBe(true) + + // Test signing the deposit transaction using wallet.sign() + try { + const signedDeposit = await testWallet!.sign(depositTx) + expect(signedDeposit).toBeDefined() + + // Send the signed transaction to supersim + const depositTxHash = await testWallet!.send(signedDeposit, publicClient) + expect(depositTxHash).toMatch(/^0x[0-9a-fA-F]{64}$/) // Valid tx hash format + + // Wait for deposit to be mined + await publicClient.waitForTransactionReceipt({ hash: depositTxHash }) + } catch { + // This is expected if Privy wallet doesn't have gas on the right network + } + + // Check vault balance after deposit attempts + const vaultBalanceAfter = await verbs.lend.getVaultBalance( + TEST_VAULT_ADDRESS, + TEST_WALLET_ADDRESS, + ) + + // For now, we expect the test to fail at signing since Privy needs proper setup + // In production, the balance would increase after successful deposits + expect(vaultBalanceBefore).toBeDefined() + expect(vaultBalanceAfter).toBeDefined() + }, 60000) + + it('should handle different human-readable amounts', async () => { + // Test fractional amounts + const tx1 = await testWallet!.lend(0.5, 'usdc', TEST_VAULT_ADDRESS) + const expectedAmount1 = parseUnits('0.5', 6) // 0.5 USDC + expect(tx1.amount).toBe(expectedAmount1) + + // Test large amounts + const tx2 = await testWallet!.lend(1000, 'usdc', TEST_VAULT_ADDRESS) + const expectedAmount2 = parseUnits('1000', 6) // 1000 USDC + expect(tx2.amount).toBe(expectedAmount2) + + // Test using address instead of symbol + const tx3 = await testWallet!.lend(1, USDC_ADDRESS, TEST_VAULT_ADDRESS) + const expectedAmount3 = parseUnits('1', 6) // 1 USDC + expect(tx3.amount).toBe(expectedAmount3) + expect(tx3.asset).toBe(USDC_ADDRESS) + }, 30000) + + it('should validate input parameters', async () => { + // Test invalid amount + await expect(testWallet!.lend(0, 'usdc')).rejects.toThrow( + 'Amount must be greater than 0', + ) + await expect(testWallet!.lend(-1, 'usdc')).rejects.toThrow( + 'Amount must be greater than 0', + ) + + // Test invalid asset symbol + await expect(testWallet!.lend(1, 'invalid')).rejects.toThrow( + 'Unsupported asset symbol: invalid', + ) + + // Test invalid address format + await expect(testWallet!.lend(1, 'not-an-address')).rejects.toThrow( + 'Unsupported asset symbol', + ) + }, 30000) +}) diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index 06c21fd7..e4fb7aea 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -78,6 +78,8 @@ export class Verbs { if (config.embeddedWalletConfig.provider.type === 'privy') { this.embeddedWalletProvider = new PrivyEmbeddedWalletProvider( config.embeddedWalletConfig.provider.privyClient, + this._chainManager, + this.lendProvider!, ) } else { throw new Error( diff --git a/packages/sdk/src/wallet/PrivyWallet.ts b/packages/sdk/src/wallet/PrivyWallet.ts index 854a78b9..19a123c7 100644 --- a/packages/sdk/src/wallet/PrivyWallet.ts +++ b/packages/sdk/src/wallet/PrivyWallet.ts @@ -1,7 +1,16 @@ import type { PrivyClient } from '@privy-io/server-auth' import type { Address, Hash, LocalAccount } from 'viem' import { toAccount } from 'viem/accounts' +import { unichain } from 'viem/chains' +import type { ChainManager } from '@/services/ChainManager.js' +import type { + LendOptions, + LendProvider, + LendTransaction, + TransactionData, +} from '@/types/lend.js' +import { type AssetIdentifier, parseLendParams } from '@/utils/assets.js' import { EmbeddedWallet } from '@/wallet/base/EmbeddedWallet.js' /** @@ -11,16 +20,26 @@ import { EmbeddedWallet } from '@/wallet/base/EmbeddedWallet.js' export class PrivyWallet extends EmbeddedWallet { public walletId: string private privyClient: PrivyClient + private chainManager: ChainManager + private lendProvider: LendProvider /** * Create a new Privy wallet provider * @param appId - Privy application ID * @param appSecret - Privy application secret * @param verbs - Verbs instance for accessing configured providers */ - constructor(privyClient: PrivyClient, walletId: string, address: Address) { + constructor( + privyClient: PrivyClient, + walletId: string, + address: Address, + chainManager: ChainManager, + lendProvider: LendProvider, + ) { super(address) this.privyClient = privyClient this.walletId = walletId + this.chainManager = chainManager + this.lendProvider = lendProvider } /** @@ -65,4 +84,145 @@ export class PrivyWallet extends EmbeddedWallet { }, }) } + + // TODO: Decide whether any of the methods below this commentare needed. This is tech debt from the POC. + + /** + * Lend assets to a lending market + * @description Lends assets using the configured lending provider with human-readable amounts + * @param amount - Human-readable amount to lend (e.g. 1.5) + * @param asset - Asset symbol (e.g. 'usdc') or token address + * @param marketId - Optional specific market ID or vault name + * @param options - Optional lending configuration + * @returns Promise resolving to lending transaction details + * @throws Error if no lending provider is configured + */ + async lend( + amount: number, + asset: AssetIdentifier, + marketId?: string, + options?: LendOptions, + ): Promise { + // Parse human-readable inputs + // TODO: Get actual chain ID from wallet context, for now using Unichain + const { amount: parsedAmount, asset: resolvedAsset } = parseLendParams( + amount, + asset, + unichain.id, + ) + + // Set receiver to wallet address if not specified + const lendOptions: LendOptions = { + ...options, + receiver: options?.receiver || this.address, + } + + const result = await this.lendProvider.deposit( + resolvedAsset.address, + parsedAmount, + marketId, + lendOptions, + ) + + return result + } + + /** + * Sign a transaction without sending it + * @description Signs a transaction using the configured wallet provider but doesn't send it + * @param transactionData - Transaction data to sign + * @returns Promise resolving to signed transaction + * @throws Error if wallet is not initialized or no wallet provider is configured + */ + async sign(transactionData: TransactionData): Promise<`0x${string}`> { + return (await this.signOnly(transactionData)) as `0x${string}` + } + + /** + * Sign a transaction without sending it + * @description Signs a transaction using Privy's wallet API but doesn't send it + * @param walletId - Wallet ID to use for signing + * @param transactionData - Transaction data to sign + * @returns Promise resolving to signed transaction + * @throws Error if transaction signing fails + */ + async signOnly(transactionData: TransactionData): Promise { + try { + const privyWallet = await this.privyClient.walletApi.getWallet({ + id: this.walletId, + }) + // Get public client for gas estimation + const publicClient = this.chainManager.getPublicClient(unichain.id) // Unichain + + // Estimate gas limit + const gasLimit = await publicClient.estimateGas({ + account: privyWallet.address as Address, + to: transactionData.to, + data: transactionData.data as `0x${string}`, + value: BigInt(transactionData.value), + }) + + // Get current gas price and fee data + const feeData = await publicClient.estimateFeesPerGas() + + // Get current nonce for the wallet - manual management since Privy isn't handling it properly + const nonce = await publicClient.getTransactionCount({ + address: privyWallet.address as Address, + blockTag: 'pending', // Use pending to get the next nonce including any pending txs + }) + + // According to Privy docs: if you provide ANY gas parameters, you must provide ALL of them + const txParams: any = { + to: transactionData.to, + data: transactionData.data as `0x${string}`, + value: transactionData.value, + chainId: 130, // Unichain + type: 2, // EIP-1559 + gasLimit: `0x${gasLimit.toString(16)}`, + maxFeePerGas: `0x${(feeData.maxFeePerGas || BigInt(1000000000)).toString(16)}`, // fallback to 1 gwei + maxPriorityFeePerGas: `0x${(feeData.maxPriorityFeePerGas || BigInt(100000000)).toString(16)}`, // fallback to 0.1 gwei + 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.privyClient.walletApi.ethereum.signTransaction({ + walletId: this.walletId, + transaction: txParams, + }) + + return response.signedTransaction + } catch (error) { + throw new Error( + `Failed to sign transaction for wallet ${this.walletId}: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + ) + } + } + + /** + * Send a signed transaction + * @description Sends a pre-signed transaction to the network + * @param signedTransaction - Signed transaction to send + * @param publicClient - Viem public client to send the transaction + * @returns Promise resolving to transaction hash + */ + async send(signedTransaction: string, publicClient: any): Promise { + try { + const hash = await publicClient.sendRawTransaction({ + serializedTransaction: signedTransaction as `0x${string}`, + }) + return hash + } catch (error) { + throw new Error( + `Failed to send transaction: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + ) + } + } } diff --git a/packages/sdk/src/wallet/providers/privy.ts b/packages/sdk/src/wallet/providers/privy.ts index 1b6c8ab6..5d5b8601 100644 --- a/packages/sdk/src/wallet/providers/privy.ts +++ b/packages/sdk/src/wallet/providers/privy.ts @@ -1,6 +1,8 @@ import type { PrivyClient } from '@privy-io/server-auth' import { getAddress } from 'viem' +import type { ChainManager } from '@/services/ChainManager.js' +import type { LendProvider } from '@/types/lend.js' import { PrivyWallet } from '@/wallet/PrivyWallet.js' import { EmbeddedWalletProvider } from '@/wallet/providers/base/EmbeddedWalletProvider.js' @@ -23,14 +25,21 @@ export interface PrivyProviderGetAllWalletsOptions { */ export class PrivyEmbeddedWalletProvider extends EmbeddedWalletProvider { public privy: PrivyClient - + private chainManager: ChainManager + private lendProvider: LendProvider /** * Create a new Privy wallet provider * @param privyClient - Privy client instance */ - constructor(privyClient: PrivyClient) { + constructor( + privyClient: PrivyClient, + chainManager: ChainManager, + lendProvider: LendProvider, + ) { super() this.privy = privyClient + this.chainManager = chainManager + this.lendProvider = lendProvider } /** @@ -49,6 +58,8 @@ export class PrivyEmbeddedWalletProvider extends EmbeddedWalletProvider { this.privy, wallet.id, getAddress(wallet.address), + this.chainManager, + this.lendProvider, ) return walletInstance } catch { @@ -72,6 +83,8 @@ export class PrivyEmbeddedWalletProvider extends EmbeddedWalletProvider { this.privy, wallet.id, getAddress(wallet.address), + this.chainManager, + this.lendProvider, ) return walletInstance } catch { @@ -99,6 +112,8 @@ export class PrivyEmbeddedWalletProvider extends EmbeddedWalletProvider { this.privy, wallet.id, getAddress(wallet.address), + this.chainManager, + this.lendProvider, ) return walletInstance }) From d312f0ffb722d0864682a08a2505e669bf5a782c Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 17:35:46 -0700 Subject: [PATCH 27/39] add warning comment --- packages/sdk/src/wallet/PrivyWallet.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/wallet/PrivyWallet.ts b/packages/sdk/src/wallet/PrivyWallet.ts index 19a123c7..ac314de4 100644 --- a/packages/sdk/src/wallet/PrivyWallet.ts +++ b/packages/sdk/src/wallet/PrivyWallet.ts @@ -85,7 +85,17 @@ export class PrivyWallet extends EmbeddedWallet { }) } - // TODO: Decide whether any of the methods below this commentare needed. This is tech debt from the POC. + // ⚠️ WARNING: TECH DEBT BELOW ⚠️ + // ===================================== + // The methods below this comment are legacy tech debt from the POC + // and will most likely be REMOVED in a future refactor. + // + // DO NOT rely on these methods in production code! + // DO NOT extend or modify these methods! + // + // If you need this functionality, please discuss with the team + // before using or building upon these methods. + // ===================================== /** * Lend assets to a lending market From 57af857eed6a0f2d26d0bd7daa62c74d62c3b4d8 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 17:41:10 -0700 Subject: [PATCH 28/39] remove unused tests --- packages/sdk/src/wallet/index.spec.ts | 163 ---------------------- packages/sdk/src/wallet/providers/test.ts | 125 ----------------- 2 files changed, 288 deletions(-) delete mode 100644 packages/sdk/src/wallet/index.spec.ts delete mode 100644 packages/sdk/src/wallet/providers/test.ts diff --git a/packages/sdk/src/wallet/index.spec.ts b/packages/sdk/src/wallet/index.spec.ts deleted file mode 100644 index e062ac99..00000000 --- a/packages/sdk/src/wallet/index.spec.ts +++ /dev/null @@ -1,163 +0,0 @@ -// import type { Address } from 'viem' -// import { unichain } from 'viem/chains' -// import { describe, expect, it } from 'vitest' - -// import type { ChainManager } from '@/services/ChainManager.js' -// import { MockChainManager } from '@/test/MockChainManager.js' -// import type { LendProvider } from '@/types/lend.js' -// import type { VerbsInterface } from '@/types/verbs.js' -// import { Wallet } from '@/wallet/index.js' - -// describe('Wallet', () => { -// const mockAddress: Address = '0x1234567890123456789012345678901234567890' -// const mockId = 'test-wallet-id' -// const chainManager: ChainManager = new MockChainManager({ -// supportedChains: [unichain.id], -// defaultBalance: 1000000n, -// }) as any - -// // Mock Verbs instance -// const mockVerbs: VerbsInterface = { -// get chainManager() { -// return chainManager -// }, -// get lend(): LendProvider { -// throw new Error('Lend provider not configured') -// }, -// createWallet: async () => { -// throw new Error('Not implemented') -// }, -// getWallet: async () => { -// throw new Error('Not implemented') -// }, -// getAllWallets: async () => { -// throw new Error('Not implemented') -// }, -// } as VerbsInterface - -// describe('constructor', () => { -// it('should create a wallet instance with correct properties', () => { -// const wallet = new Wallet(mockId, mockVerbs) -// wallet.init(mockAddress) - -// expect(wallet.id).toBe(mockId) -// expect(wallet.address).toBe(mockAddress) -// }) - -// it('should handle different wallet IDs', () => { -// const ids = ['wallet-1', 'wallet-2', 'test-id-123'] - -// ids.forEach((id) => { -// const wallet = new Wallet(id, mockVerbs) -// expect(wallet.id).toBe(id) -// }) -// }) -// }) - -// describe('address assignment', () => { -// it('should allow setting address after creation', () => { -// const addresses: Address[] = [ -// '0x0000000000000000000000000000000000000000', -// '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF', -// '0x742d35Cc6634C0532925a3b8C17Eb02c7b2BD8eB', -// ] - -// addresses.forEach((address) => { -// const wallet = new Wallet(mockId, mockVerbs) -// wallet.init(address) -// expect(wallet.address).toBe(address) -// expect(wallet.id).toBe(mockId) -// }) -// }) -// }) - -// describe('getBalance', () => { -// it('should return token balances', async () => { -// const wallet = new Wallet(mockId, mockVerbs) -// wallet.init(mockAddress) - -// const balance = await wallet.getBalance() - -// expect(balance).toEqual([ -// { -// totalBalance: 1000000n, -// symbol: 'ETH', -// totalFormattedBalance: '0.000000000001', -// chainBalances: [ -// { -// balance: 1000000n, -// chainId: 130, -// formattedBalance: '0.000000000001', -// }, -// ], -// }, -// { -// totalBalance: 1000000n, -// symbol: 'ETH', -// totalFormattedBalance: '0.000000000001', -// chainBalances: [ -// { -// balance: 1000000n, -// chainId: 130, -// formattedBalance: '0.000000000001', -// }, -// ], -// }, -// { -// totalBalance: 1000000n, -// symbol: 'USDC', -// totalFormattedBalance: '1', -// chainBalances: [ -// { -// balance: 1000000n, -// chainId: 130, -// formattedBalance: '1', -// }, -// ], -// }, -// { -// totalBalance: 0n, -// symbol: 'MORPHO', -// totalFormattedBalance: '0', -// chainBalances: [], -// }, -// ]) -// }) - -// it('should throw an error if the wallet is not initialized', async () => { -// const wallet = new Wallet(mockId, mockVerbs) -// await expect(wallet.getBalance()).rejects.toThrow( -// 'Wallet not initialized', -// ) -// }) -// }) - -// describe('edge cases', () => { -// it('should handle empty string id', () => { -// const wallet = new Wallet('', mockVerbs) -// expect(wallet.id).toBe('') -// expect(wallet.address).toBeUndefined() -// }) - -// it('should handle very long wallet id', () => { -// const longId = 'a'.repeat(1000) -// const wallet = new Wallet(longId, mockVerbs) -// expect(wallet.id).toBe(longId) -// expect(wallet.id.length).toBe(1000) -// }) -// }) - -// describe('immutability', () => { -// it('should maintain property values after creation', () => { -// const wallet = new Wallet(mockId, mockVerbs) -// wallet.address = mockAddress - -// const originalId = wallet.id -// const originalAddress = wallet.address - -// // Properties should remain unchanged -// expect(wallet.id).toBe(originalId) -// expect(wallet.address).toBe(originalAddress) -// }) -// }) -// }) diff --git a/packages/sdk/src/wallet/providers/test.ts b/packages/sdk/src/wallet/providers/test.ts deleted file mode 100644 index 690647ee..00000000 --- a/packages/sdk/src/wallet/providers/test.ts +++ /dev/null @@ -1,125 +0,0 @@ -// import { type Address, type Hash, type WalletClient } from 'viem' - -// import type { TransactionData } from '@/types/lend.js' -// import type { VerbsInterface } from '@/types/verbs.js' -// import type { -// GetAllWalletsOptions, -// Wallet, -// WalletProvider, -// } from '@/types/wallet.js' -// import { Wallet as WalletImpl } from '@/wallet/index.js' - -// /** -// * Test wallet provider for local testing with viem -// * @description Test implementation of WalletProvider that uses viem directly -// */ -// export class WalletProviderTest implements WalletProvider { -// private walletClient: WalletClient -// private verbs: VerbsInterface -// private wallets: Map = new Map() - -// constructor(walletClient: WalletClient, verbs: VerbsInterface) { -// this.walletClient = walletClient -// this.verbs = verbs -// } - -// /** -// * Create a new wallet (or register existing) -// */ -// async createWallet(userId: string): Promise { -// // For testing, we'll use the wallet client's account -// const [address] = await this.walletClient.getAddresses() - -// this.wallets.set(userId, { id: userId, address }) - -// const wallet = new WalletImpl(userId, this.verbs, this) -// wallet.init(address) -// return wallet -// } - -// /** -// * Get wallet by user ID -// */ -// async getWallet(userId: string): Promise { -// const walletData = this.wallets.get(userId) -// if (!walletData) return null - -// const wallet = new WalletImpl(userId, this.verbs, this) -// wallet.init(walletData.address) -// return wallet -// } - -// /** -// * Get all wallets -// */ -// async getAllWallets(_options?: GetAllWalletsOptions): Promise { -// const wallets: Wallet[] = [] - -// for (const [userId, walletData] of this.wallets.entries()) { -// const wallet = new WalletImpl(userId, this.verbs, this) -// wallet.init(walletData.address) -// wallets.push(wallet) -// } - -// return wallets -// } - -// /** -// * Sign and send a transaction using viem wallet client -// */ -// async sign( -// _walletId: string, -// transactionData: TransactionData, -// ): Promise { -// try { -// // Send transaction using viem wallet client -// const txParams: any = { -// to: transactionData.to as Address, -// data: transactionData.data as `0x${string}`, -// value: BigInt(transactionData.value), -// } - -// // Add account if available -// if (this.walletClient.account) { -// txParams.account = this.walletClient.account -// } - -// const hash = await this.walletClient.sendTransaction(txParams) - -// return hash -// } catch (error) { -// throw new Error( -// `Failed to sign transaction: ${ -// error instanceof Error ? error.message : 'Unknown error' -// }`, -// ) -// } -// } - -// /** -// * Sign a transaction without sending it -// */ -// async signOnly( -// _walletId: string, -// transactionData: TransactionData, -// ): Promise { -// try { -// // Sign transaction using viem wallet client -// const txParams: any = { -// to: transactionData.to as Address, -// data: transactionData.data as `0x${string}`, -// value: BigInt(transactionData.value), -// } - -// const signedTx = await this.walletClient.signTransaction(txParams) - -// return signedTx -// } catch (error) { -// throw new Error( -// `Failed to sign transaction: ${ -// error instanceof Error ? error.message : 'Unknown error' -// }`, -// ) -// } -// } -// } From 83708f4f4305815bb15b6930a7f0d969fe06a278 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 17:41:52 -0700 Subject: [PATCH 29/39] remove unused tests --- packages/sdk/src/wallet/wallet.test.ts | 187 ------------------------- 1 file changed, 187 deletions(-) delete mode 100644 packages/sdk/src/wallet/wallet.test.ts diff --git a/packages/sdk/src/wallet/wallet.test.ts b/packages/sdk/src/wallet/wallet.test.ts deleted file mode 100644 index 32956b33..00000000 --- a/packages/sdk/src/wallet/wallet.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -// import { type Address, encodeFunctionData, erc20Abi, parseUnits } from 'viem' -// import { unichain } from 'viem/chains' -// import { beforeEach, describe, expect, it } from 'vitest' - -// import type { VerbsInterface } from '../types/verbs.js' -// import { initVerbs } from '../verbs.js' -// import { Wallet } from './index.js' - -// describe('Wallet SendTokens', () => { -// let verbs: VerbsInterface -// let wallet: Wallet -// const testAddress: Address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' -// const recipientAddress: Address = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' - -// beforeEach(() => { -// // Initialize Verbs SDK -// verbs = initVerbs({ -// wallet: { -// type: 'privy', -// appId: 'test-app-id', -// appSecret: 'test-app-secret', -// }, -// chains: [ -// { -// chainId: unichain.id, -// rpcUrl: 'http://localhost:9546', -// }, -// ], -// }) - -// // Create wallet instance -// wallet = new Wallet('test-wallet', verbs) -// wallet.init(testAddress) -// }) - -// describe('ETH transfers', () => { -// it('should create valid ETH transfer transaction data', async () => { -// const transferData = await wallet.sendTokens(1.5, 'eth', recipientAddress) - -// expect(transferData).toEqual({ -// to: recipientAddress, -// value: `0x${parseUnits('1.5', 18).toString(16)}`, -// data: '0x', -// }) -// }) - -// it('should handle fractional ETH amounts correctly', async () => { -// const transferData = await wallet.sendTokens( -// 0.001, -// 'eth', -// recipientAddress, -// ) -// const expectedValue = parseUnits('0.001', 18) - -// expect(transferData.to).toBe(recipientAddress) -// expect(transferData.value).toBe(`0x${expectedValue.toString(16)}`) -// expect(transferData.data).toBe('0x') -// }) - -// it('should handle large ETH amounts correctly', async () => { -// const transferData = await wallet.sendTokens(100, 'eth', recipientAddress) -// const expectedValue = parseUnits('100', 18) - -// expect(transferData.to).toBe(recipientAddress) -// expect(transferData.value).toBe(`0x${expectedValue.toString(16)}`) -// expect(transferData.data).toBe('0x') -// }) -// }) - -// describe('USDC transfers', () => { -// const usdcAddress = '0x078d782b760474a361dda0af3839290b0ef57ad6' // USDC on Unichain - -// it('should create valid USDC transfer transaction data', async () => { -// const transferData = await wallet.sendTokens( -// 100, -// 'usdc', -// recipientAddress, -// ) -// const expectedAmount = parseUnits('100', 6) // USDC has 6 decimals - -// const expectedData = encodeFunctionData({ -// abi: erc20Abi, -// functionName: 'transfer', -// args: [recipientAddress, expectedAmount], -// }) - -// expect(transferData).toEqual({ -// to: usdcAddress, -// value: '0x0', -// data: expectedData, -// }) -// }) - -// it('should handle fractional USDC amounts correctly', async () => { -// const transferData = await wallet.sendTokens( -// 0.5, -// 'usdc', -// recipientAddress, -// ) -// const expectedAmount = parseUnits('0.5', 6) // USDC has 6 decimals - -// const expectedData = encodeFunctionData({ -// abi: erc20Abi, -// functionName: 'transfer', -// args: [recipientAddress, expectedAmount], -// }) - -// expect(transferData.to).toBe(usdcAddress) -// expect(transferData.value).toBe('0x0') -// expect(transferData.data).toBe(expectedData) -// }) - -// it('should handle USDC by token address', async () => { -// const transferData = await wallet.sendTokens( -// 50, -// usdcAddress, -// recipientAddress, -// ) -// const expectedAmount = parseUnits('50', 6) // USDC has 6 decimals - -// const expectedData = encodeFunctionData({ -// abi: erc20Abi, -// functionName: 'transfer', -// args: [recipientAddress, expectedAmount], -// }) - -// expect(transferData.to).toBe(usdcAddress) -// expect(transferData.value).toBe('0x0') -// expect(transferData.data).toBe(expectedData) -// }) -// }) - -// describe('validation', () => { -// it('should throw error if wallet is not initialized', async () => { -// const uninitializedWallet = new Wallet('test-wallet', verbs) - -// await expect( -// uninitializedWallet.sendTokens(1, 'eth', recipientAddress), -// ).rejects.toThrow('Wallet not initialized') -// }) - -// it('should throw error for zero amount', async () => { -// await expect( -// wallet.sendTokens(0, 'eth', recipientAddress), -// ).rejects.toThrow('Amount must be greater than 0') -// }) - -// it('should throw error for negative amount', async () => { -// await expect( -// wallet.sendTokens(-1, 'eth', recipientAddress), -// ).rejects.toThrow('Amount must be greater than 0') -// }) - -// it('should throw error for empty recipient address', async () => { -// await expect(wallet.sendTokens(1, 'eth', '' as Address)).rejects.toThrow( -// 'Recipient address is required', -// ) -// }) - -// it('should throw error for unsupported asset symbol', async () => { -// await expect( -// wallet.sendTokens(1, 'invalid-token', recipientAddress), -// ).rejects.toThrow('Unsupported asset symbol: invalid-token') -// }) - -// it('should throw error for invalid token address', async () => { -// await expect( -// wallet.sendTokens(1, '0xinvalid', recipientAddress), -// ).rejects.toThrow('Unknown asset address') -// }) -// }) - -// describe('asset symbol case insensitivity', () => { -// it('should handle uppercase ETH', async () => { -// const transferData = await wallet.sendTokens(1, 'ETH', recipientAddress) -// expect(transferData.to).toBe(recipientAddress) -// expect(transferData.data).toBe('0x') -// }) - -// it('should handle mixed case USDC', async () => { -// const usdcAddress = '0x078d782b760474a361dda0af3839290b0ef57ad6' -// const transferData = await wallet.sendTokens(1, 'UsDc', recipientAddress) -// expect(transferData.to).toBe(usdcAddress) -// expect(transferData.value).toBe('0x0') -// }) -// }) -// }) From 897718d7b1b714559e54948607371ac3a044174a Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 17:45:36 -0700 Subject: [PATCH 30/39] lint --- packages/sdk/src/wallet/PrivyWallet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/wallet/PrivyWallet.ts b/packages/sdk/src/wallet/PrivyWallet.ts index ac314de4..d9285aaf 100644 --- a/packages/sdk/src/wallet/PrivyWallet.ts +++ b/packages/sdk/src/wallet/PrivyWallet.ts @@ -89,10 +89,10 @@ export class PrivyWallet extends EmbeddedWallet { // ===================================== // The methods below this comment are legacy tech debt from the POC // and will most likely be REMOVED in a future refactor. - // + // // DO NOT rely on these methods in production code! // DO NOT extend or modify these methods! - // + // // If you need this functionality, please discuss with the team // before using or building upon these methods. // ===================================== From 9a8dcf4e87e42fdb5f7cb90aae82f554a229bc09 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 17:48:45 -0700 Subject: [PATCH 31/39] fix frontend tests --- packages/demo/frontend/src/components/Terminal.spec.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/demo/frontend/src/components/Terminal.spec.tsx b/packages/demo/frontend/src/components/Terminal.spec.tsx index 9f376b95..df1cb464 100644 --- a/packages/demo/frontend/src/components/Terminal.spec.tsx +++ b/packages/demo/frontend/src/components/Terminal.spec.tsx @@ -9,7 +9,8 @@ vi.mock('../api/verbsApi', () => ({ verbsApi: { createWallet: vi.fn(() => Promise.resolve({ - address: '0x1234567890123456789012345678901234567890', + privyAddress: '0x1234567890123456789012345678901234567890', + smartWalletAddress: '0x1234567890123456789012345678901234567890', userId: 'test-user', }) ), From 7cd3dfe1b1c0ebe7b898c4089b32faf5d7031323 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 18:14:19 -0700 Subject: [PATCH 32/39] fix backend tests --- .../demo/backend/src/services/wallet.spec.ts | 170 ++++++++++++++---- packages/demo/backend/src/services/wallet.ts | 8 +- 2 files changed, 143 insertions(+), 35 deletions(-) diff --git a/packages/demo/backend/src/services/wallet.spec.ts b/packages/demo/backend/src/services/wallet.spec.ts index b4881d2f..3996b11b 100644 --- a/packages/demo/backend/src/services/wallet.spec.ts +++ b/packages/demo/backend/src/services/wallet.spec.ts @@ -4,9 +4,21 @@ import * as walletService from './wallet.js' // Mock the Verbs SDK const mockVerbs = { - createWallet: vi.fn(), - getWallet: vi.fn(), - getAllWallets: vi.fn(), + wallet: { + embeddedWalletProvider: { + getAllWallets: vi.fn(), + }, + smartWalletProvider: { + getWalletAddress: vi.fn(), + getWallet: vi.fn(), + }, + getSmartWallet: vi.fn(), + getEmbeddedWallet: vi.fn(), + getSmartWalletWithEmbeddedSigner: vi.fn(), + createWalletWithEmbeddedSigner: vi.fn(), + createSmartWallet: vi.fn(), + createEmbeddedWallet: vi.fn(), + }, } // Mock the getVerbs function @@ -21,24 +33,35 @@ describe('Wallet Service', () => { describe('createWallet', () => { it('should create a wallet using the Verbs SDK', async () => { - const userId = 'test-user' const mockWallet = { id: 'wallet-123', - address: '0x1234567890123456789012345678901234567890', + getAddress: vi + .fn() + .mockResolvedValue('0x1234567890123456789012345678901234567890'), + signer: { + address: '0x1234567890123456789012345678901234567890', + }, } - mockVerbs.createWallet.mockResolvedValue(mockWallet) + mockVerbs.wallet.createWalletWithEmbeddedSigner.mockResolvedValue( + mockWallet, + ) const result = await walletService.createWallet() - expect(mockVerbs.createWallet).toHaveBeenCalledWith(userId) - expect(result).toEqual(mockWallet) + expect( + mockVerbs.wallet.createWalletWithEmbeddedSigner, + ).toHaveBeenCalledWith() + expect(result).toEqual({ + privyAddress: '0x1234567890123456789012345678901234567890', + smartWalletAddress: '0x1234567890123456789012345678901234567890', + }) }) it('should handle wallet creation errors', async () => { const error = new Error('Wallet creation failed') - mockVerbs.createWallet.mockRejectedValue(error) + mockVerbs.wallet.createWalletWithEmbeddedSigner.mockRejectedValue(error) await expect(walletService.createWallet()).rejects.toThrow( 'Wallet creation failed', @@ -54,30 +77,40 @@ describe('Wallet Service', () => { address: '0x1234567890123456789012345678901234567890', } - mockVerbs.getWallet.mockResolvedValue(mockWallet) + mockVerbs.wallet.getSmartWalletWithEmbeddedSigner.mockResolvedValue( + mockWallet, + ) const result = await walletService.getWallet(userId) - expect(mockVerbs.getWallet).toHaveBeenCalledWith(userId) - expect(result).toEqual(mockWallet) + expect( + mockVerbs.wallet.getSmartWalletWithEmbeddedSigner, + ).toHaveBeenCalledWith({ + walletId: userId, + }) + expect(result).toEqual({ wallet: mockWallet }) }) it('should return null if wallet not found', async () => { const userId = 'non-existent-user' - mockVerbs.getWallet.mockResolvedValue(null) + mockVerbs.wallet.getSmartWalletWithEmbeddedSigner.mockResolvedValue(null) const result = await walletService.getWallet(userId) - expect(mockVerbs.getWallet).toHaveBeenCalledWith(userId) - expect(result).toBeNull() + expect( + mockVerbs.wallet.getSmartWalletWithEmbeddedSigner, + ).toHaveBeenCalledWith({ + walletId: userId, + }) + expect(result).toEqual({ wallet: null }) }) it('should handle wallet retrieval errors', async () => { const userId = 'test-user' const error = new Error('Wallet retrieval failed') - mockVerbs.getWallet.mockRejectedValue(error) + mockVerbs.wallet.getSmartWalletWithEmbeddedSigner.mockRejectedValue(error) await expect(walletService.getWallet(userId)).rejects.toThrow( 'Wallet retrieval failed', @@ -87,44 +120,95 @@ describe('Wallet Service', () => { describe('getAllWallets', () => { it('should get all wallets without options', async () => { - const mockWallets = [ + const mockPrivyWallets = [ { - id: 'wallet-1', + walletId: 'wallet-1', address: '0x1234567890123456789012345678901234567890', + signer: vi.fn().mockResolvedValue({ + address: '0x1234567890123456789012345678901234567890', + }), }, { - id: 'wallet-2', + walletId: 'wallet-2', address: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + signer: vi.fn().mockResolvedValue({ + address: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + }), }, ] - mockVerbs.getAllWallets.mockResolvedValue(mockWallets) + mockVerbs.wallet.embeddedWalletProvider.getAllWallets.mockResolvedValue( + mockPrivyWallets, + ) + mockVerbs.wallet.smartWalletProvider.getWalletAddress.mockResolvedValue( + '0x1234567890123456789012345678901234567890', + ) + mockVerbs.wallet.smartWalletProvider.getWallet.mockReturnValue({ + address: '0x1234567890123456789012345678901234567890', + }) const result = await walletService.getAllWallets() - expect(mockVerbs.getAllWallets).toHaveBeenCalledWith(undefined) - expect(result).toEqual(mockWallets) + expect( + mockVerbs.wallet.embeddedWalletProvider.getAllWallets, + ).toHaveBeenCalledWith(undefined) + expect(result).toEqual([ + { + wallet: { + address: '0x1234567890123456789012345678901234567890', + }, + id: 'wallet-1', + }, + { + wallet: { + address: '0x1234567890123456789012345678901234567890', + }, + id: 'wallet-2', + }, + ]) }) it('should get all wallets with options', async () => { const mockWallets = [ { - id: 'wallet-1', + walletId: 'wallet-1', address: '0x1234567890123456789012345678901234567890', + signer: vi.fn().mockResolvedValue({ + address: '0x1234567890123456789012345678901234567890', + }), }, ] const options = { limit: 1, cursor: 'cursor-123' } - mockVerbs.getAllWallets.mockResolvedValue(mockWallets) + mockVerbs.wallet.embeddedWalletProvider.getAllWallets.mockResolvedValue( + mockWallets, + ) + mockVerbs.wallet.smartWalletProvider.getWalletAddress.mockResolvedValue( + '0x1234567890123456789012345678901234567890', + ) + mockVerbs.wallet.smartWalletProvider.getWallet.mockReturnValue({ + address: '0x1234567890123456789012345678901234567890', + }) const result = await walletService.getAllWallets(options) - expect(mockVerbs.getAllWallets).toHaveBeenCalledWith(options) - expect(result).toEqual(mockWallets) + expect( + mockVerbs.wallet.embeddedWalletProvider.getAllWallets, + ).toHaveBeenCalledWith(options) + expect(result).toEqual([ + { + wallet: { + address: '0x1234567890123456789012345678901234567890', + }, + id: 'wallet-1', + }, + ]) }) it('should handle empty wallet list', async () => { - mockVerbs.getAllWallets.mockResolvedValue([]) + mockVerbs.wallet.embeddedWalletProvider.getAllWallets.mockResolvedValue( + [], + ) const result = await walletService.getAllWallets() @@ -134,7 +218,9 @@ describe('Wallet Service', () => { it('should handle getAllWallets errors', async () => { const error = new Error('Failed to get all wallets') - mockVerbs.getAllWallets.mockRejectedValue(error) + mockVerbs.wallet.embeddedWalletProvider.getAllWallets.mockRejectedValue( + error, + ) await expect(walletService.getAllWallets()).rejects.toThrow( 'Failed to get all wallets', @@ -154,11 +240,17 @@ describe('Wallet Service', () => { ]), } - mockVerbs.getWallet.mockResolvedValue(mockWallet) + mockVerbs.wallet.getSmartWalletWithEmbeddedSigner.mockResolvedValue( + mockWallet, + ) const result = await walletService.getBalance(userId) - expect(mockVerbs.getWallet).toHaveBeenCalledWith(userId) + expect( + mockVerbs.wallet.getSmartWalletWithEmbeddedSigner, + ).toHaveBeenCalledWith({ + walletId: userId, + }) expect(mockWallet.getBalance).toHaveBeenCalled() expect(result).toEqual([ { symbol: 'USDC', balance: 1000000n }, @@ -169,13 +261,17 @@ describe('Wallet Service', () => { it('should throw error when wallet not found', async () => { const userId = 'non-existent-user' - mockVerbs.getWallet.mockResolvedValue(null) + mockVerbs.wallet.getSmartWalletWithEmbeddedSigner.mockResolvedValue(null) await expect(walletService.getBalance(userId)).rejects.toThrow( 'Wallet not found', ) - expect(mockVerbs.getWallet).toHaveBeenCalledWith(userId) + expect( + mockVerbs.wallet.getSmartWalletWithEmbeddedSigner, + ).toHaveBeenCalledWith({ + walletId: userId, + }) }) it('should handle balance retrieval errors', async () => { @@ -187,13 +283,19 @@ describe('Wallet Service', () => { getBalance: vi.fn().mockRejectedValue(balanceError), } - mockVerbs.getWallet.mockResolvedValue(mockWallet) + mockVerbs.wallet.getSmartWalletWithEmbeddedSigner.mockResolvedValue( + mockWallet, + ) await expect(walletService.getBalance(userId)).rejects.toThrow( 'Balance retrieval failed', ) - expect(mockVerbs.getWallet).toHaveBeenCalledWith(userId) + expect( + mockVerbs.wallet.getSmartWalletWithEmbeddedSigner, + ).toHaveBeenCalledWith({ + walletId: userId, + }) expect(mockWallet.getBalance).toHaveBeenCalled() }) }) diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index a4c0a00a..b85b68b5 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -60,7 +60,9 @@ export async function getAllWallets( await verbs.wallet.smartWalletProvider.getWalletAddress({ owners: [privyWallet.address], }) + console.log('walletAddress', walletAddress) const signer = await privyWallet.signer() + console.log('signer', signer) const wallet = ( verbs.wallet.smartWalletProvider as SmartWalletProvider ).getWallet({ @@ -68,6 +70,7 @@ export async function getAllWallets( signer, ownerIndex: 0, }) + console.log('wallet', wallet) return { wallet, id: privyWallet.walletId, @@ -75,12 +78,15 @@ export async function getAllWallets( }), ) } catch { - throw new Error('Failed to retrieve wallets') + throw new Error('Failed to get all wallets') } } export async function getBalance(userId: string): Promise { const { wallet } = await getWallet(userId) + if (!wallet) { + throw new Error('Wallet not found') + } // Get regular token balances const tokenBalances = await wallet.getBalance().catch((error) => { From 3a78885a71579bd247430f30e7f420123bfe5641 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 18:17:39 -0700 Subject: [PATCH 33/39] fix env var --- packages/demo/backend/src/config/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/demo/backend/src/config/env.ts b/packages/demo/backend/src/config/env.ts index 016db175..8abc6f38 100644 --- a/packages/demo/backend/src/config/env.ts +++ b/packages/demo/backend/src/config/env.ts @@ -54,5 +54,5 @@ export const env = cleanEnv(process.env, { FAUCET_ADDRESS: str({ default: getFaucetAddressDefault(), }), - BASE_SEPOLIA_BUNDER_URL: str(), + BASE_SEPOLIA_BUNDER_URL: str({ default: undefined }), }) From b2adc3b562f1d487a47509882434d851ee7a306e Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 18:34:23 -0700 Subject: [PATCH 34/39] fix integration tests --- .../demo/backend/src/app.integration.spec.ts | 43 +++++++++++++++---- packages/demo/backend/src/services/wallet.ts | 3 -- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/demo/backend/src/app.integration.spec.ts b/packages/demo/backend/src/app.integration.spec.ts index fb3228a3..83c1da7b 100644 --- a/packages/demo/backend/src/app.integration.spec.ts +++ b/packages/demo/backend/src/app.integration.spec.ts @@ -12,10 +12,13 @@ import { router } from './router.js' vi.mock('./config/verbs.js', () => ({ initializeVerbs: vi.fn(), getVerbs: vi.fn(() => ({ - createWallet: vi.fn((userId: string) => + wallet:{ createWalletWithEmbeddedSigner: vi.fn(() => Promise.resolve({ - id: `wallet-${userId}`, - address: `0x${userId.padEnd(40, '0')}`, + id: `wallet-1`, + signer: { + address: `0x1111111111111111111111111111111111111111`, + }, + getAddress: () => Promise.resolve(`0x1111111111111111111111111111111111111111`), getBalance: () => Promise.resolve([ { symbol: 'USDC', balance: 1000000n }, @@ -23,14 +26,14 @@ vi.mock('./config/verbs.js', () => ({ ]), }), ), - getWallet: vi.fn((userId: string) => { + getSmartWalletWithEmbeddedSigner: vi.fn(({walletId: userId}: {walletId: string}) => { // Simulate some users existing and others not if (userId.includes('non-existent')) { return Promise.resolve(null) } return Promise.resolve({ id: `wallet-${userId}`, - address: `0x${userId.padEnd(40, '0')}`, + getAddress: () => Promise.resolve(`0x${userId.padEnd(40, '0')}`), getBalance: () => Promise.resolve([ { symbol: 'USDC', balance: 1000000n }, @@ -38,6 +41,20 @@ vi.mock('./config/verbs.js', () => ({ ]), }) }), + smartWalletProvider: { + getWalletAddress: vi.fn(({owners}: {owners: string[]}) => { + return Promise.resolve(owners[0]) + }), + getWallet: vi.fn(({walletAddress, signer, ownerIndex}: {walletAddress: string, signer: string, ownerIndex: number}) => { + return ({ + address: walletAddress, + getAddress: () => Promise.resolve(walletAddress), + signer, + ownerIndex, + }) + }), + }, + embeddedWalletProvider: { getAllWallets: vi.fn(() => Promise.resolve([ { @@ -48,6 +65,9 @@ vi.mock('./config/verbs.js', () => ({ { symbol: 'USDC', balance: 1000000n }, { symbol: 'MORPHO', balance: 500000n }, ]), + signer: vi.fn().mockResolvedValue({ + address: '0x1111111111111111111111111111111111111111', + }), }, { id: 'wallet-2', @@ -57,9 +77,13 @@ vi.mock('./config/verbs.js', () => ({ { symbol: 'USDC', balance: 2000000n }, { symbol: 'MORPHO', balance: 750000n }, ]), + signer: vi.fn().mockResolvedValue({ + address: '0x2222222222222222222222222222222222222222', + }), }, ]), - ), + )}, + }, lend: { getVaults: vi.fn(() => Promise.resolve([ @@ -186,11 +210,12 @@ describe('HTTP API Integration', () => { expect(response.statusCode).toBe(200) const data = (await response.body.json()) as any - - expect(data).toHaveProperty('address') + expect(data).toHaveProperty('privyAddress') + expect(data).toHaveProperty('smartWalletAddress') expect(data).toHaveProperty('userId') expect(data.userId).toBe(testUserId) - expect(data.address).toMatch(/^0x[a-zA-Z0-9\-]{1,}$/) // Basic address format validation + expect(data.smartWalletAddress).toMatch(/^0x[a-zA-Z0-9\-]{1,}$/) + expect(data.privyAddress).toMatch(/^0x[a-zA-Z0-9\-]{1,}$/) }) it('should get an existing wallet', async () => { diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index b85b68b5..c1a90cce 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -60,9 +60,7 @@ export async function getAllWallets( await verbs.wallet.smartWalletProvider.getWalletAddress({ owners: [privyWallet.address], }) - console.log('walletAddress', walletAddress) const signer = await privyWallet.signer() - console.log('signer', signer) const wallet = ( verbs.wallet.smartWalletProvider as SmartWalletProvider ).getWallet({ @@ -70,7 +68,6 @@ export async function getAllWallets( signer, ownerIndex: 0, }) - console.log('wallet', wallet) return { wallet, id: privyWallet.walletId, From 78afda8ce55afbc4ff78ae632e1d94afbec171a2 Mon Sep 17 00:00:00 2001 From: tre Date: Wed, 27 Aug 2025 18:38:54 -0700 Subject: [PATCH 35/39] lint --- .../demo/backend/src/app.integration.spec.ts | 151 ++++++++++-------- 1 file changed, 83 insertions(+), 68 deletions(-) diff --git a/packages/demo/backend/src/app.integration.spec.ts b/packages/demo/backend/src/app.integration.spec.ts index 83c1da7b..4cd71d5f 100644 --- a/packages/demo/backend/src/app.integration.spec.ts +++ b/packages/demo/backend/src/app.integration.spec.ts @@ -12,78 +12,93 @@ import { router } from './router.js' vi.mock('./config/verbs.js', () => ({ initializeVerbs: vi.fn(), getVerbs: vi.fn(() => ({ - wallet:{ createWalletWithEmbeddedSigner: vi.fn(() => - Promise.resolve({ - id: `wallet-1`, - signer: { - address: `0x1111111111111111111111111111111111111111`, - }, - getAddress: () => Promise.resolve(`0x1111111111111111111111111111111111111111`), - getBalance: () => - Promise.resolve([ - { symbol: 'USDC', balance: 1000000n }, - { symbol: 'MORPHO', balance: 500000n }, - ]), - }), - ), - getSmartWalletWithEmbeddedSigner: vi.fn(({walletId: userId}: {walletId: string}) => { - // Simulate some users existing and others not - if (userId.includes('non-existent')) { - return Promise.resolve(null) - } - return Promise.resolve({ - id: `wallet-${userId}`, - getAddress: () => Promise.resolve(`0x${userId.padEnd(40, '0')}`), - getBalance: () => - Promise.resolve([ - { symbol: 'USDC', balance: 1000000n }, - { symbol: 'MORPHO', balance: 500000n }, - ]), - }) - }), - smartWalletProvider: { - getWalletAddress: vi.fn(({owners}: {owners: string[]}) => { - return Promise.resolve(owners[0]) - }), - getWallet: vi.fn(({walletAddress, signer, ownerIndex}: {walletAddress: string, signer: string, ownerIndex: number}) => { - return ({ - address: walletAddress, - getAddress: () => Promise.resolve(walletAddress), - signer, - ownerIndex, - }) - }), - }, - embeddedWalletProvider: { - getAllWallets: vi.fn(() => - Promise.resolve([ - { - id: 'wallet-1', - address: '0x1111111111111111111111111111111111111111', + wallet: { + createWalletWithEmbeddedSigner: vi.fn(() => + Promise.resolve({ + id: `wallet-1`, + signer: { + address: `0x1111111111111111111111111111111111111111`, + }, + getAddress: () => + Promise.resolve(`0x1111111111111111111111111111111111111111`), getBalance: () => Promise.resolve([ { symbol: 'USDC', balance: 1000000n }, { symbol: 'MORPHO', balance: 500000n }, ]), - signer: vi.fn().mockResolvedValue({ - address: '0x1111111111111111111111111111111111111111', - }), - }, - { - id: 'wallet-2', - address: '0x2222222222222222222222222222222222222222', - getBalance: () => - Promise.resolve([ - { symbol: 'USDC', balance: 2000000n }, - { symbol: 'MORPHO', balance: 750000n }, - ]), - signer: vi.fn().mockResolvedValue({ - address: '0x2222222222222222222222222222222222222222', - }), + }), + ), + getSmartWalletWithEmbeddedSigner: vi.fn( + ({ walletId: userId }: { walletId: string }) => { + // Simulate some users existing and others not + if (userId.includes('non-existent')) { + return Promise.resolve(null) + } + return Promise.resolve({ + id: `wallet-${userId}`, + getAddress: () => Promise.resolve(`0x${userId.padEnd(40, '0')}`), + getBalance: () => + Promise.resolve([ + { symbol: 'USDC', balance: 1000000n }, + { symbol: 'MORPHO', balance: 500000n }, + ]), + }) }, - ]), - )}, - }, + ), + smartWalletProvider: { + getWalletAddress: vi.fn(({ owners }: { owners: string[] }) => { + return Promise.resolve(owners[0]) + }), + getWallet: vi.fn( + ({ + walletAddress, + signer, + ownerIndex, + }: { + walletAddress: string + signer: string + ownerIndex: number + }) => { + return { + address: walletAddress, + getAddress: () => Promise.resolve(walletAddress), + signer, + ownerIndex, + } + }, + ), + }, + embeddedWalletProvider: { + getAllWallets: vi.fn(() => + Promise.resolve([ + { + id: 'wallet-1', + address: '0x1111111111111111111111111111111111111111', + getBalance: () => + Promise.resolve([ + { symbol: 'USDC', balance: 1000000n }, + { symbol: 'MORPHO', balance: 500000n }, + ]), + signer: vi.fn().mockResolvedValue({ + address: '0x1111111111111111111111111111111111111111', + }), + }, + { + id: 'wallet-2', + address: '0x2222222222222222222222222222222222222222', + getBalance: () => + Promise.resolve([ + { symbol: 'USDC', balance: 2000000n }, + { symbol: 'MORPHO', balance: 750000n }, + ]), + signer: vi.fn().mockResolvedValue({ + address: '0x2222222222222222222222222222222222222222', + }), + }, + ]), + ), + }, + }, lend: { getVaults: vi.fn(() => Promise.resolve([ @@ -214,8 +229,8 @@ describe('HTTP API Integration', () => { expect(data).toHaveProperty('smartWalletAddress') expect(data).toHaveProperty('userId') expect(data.userId).toBe(testUserId) - expect(data.smartWalletAddress).toMatch(/^0x[a-zA-Z0-9\-]{1,}$/) - expect(data.privyAddress).toMatch(/^0x[a-zA-Z0-9\-]{1,}$/) + expect(data.smartWalletAddress).toMatch(/^0x[a-zA-Z0-9\-]{1,}$/) + expect(data.privyAddress).toMatch(/^0x[a-zA-Z0-9\-]{1,}$/) }) it('should get an existing wallet', async () => { From 5391c06a3caeaf737d6455d9f407baad9db8623a Mon Sep 17 00:00:00 2001 From: tre Date: Thu, 28 Aug 2025 11:00:37 -0700 Subject: [PATCH 36/39] rename privy provider file --- packages/sdk/src/index.ts | 2 +- packages/sdk/src/verbs.ts | 13 ++++++------- .../{privy.ts => PrivyEmbeddedWalletProvider.ts} | 2 -- 3 files changed, 7 insertions(+), 10 deletions(-) rename packages/sdk/src/wallet/providers/{privy.ts => PrivyEmbeddedWalletProvider.ts} (98%) diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 0e6f3099..2b81fb85 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -22,4 +22,4 @@ export { SmartWalletProvider } from './wallet/providers/base/SmartWalletProvider export { PrivyEmbeddedWalletProvider, type PrivyProviderGetAllWalletsOptions, -} from './wallet/providers/privy.js' +} from './wallet/providers/PrivyEmbeddedWalletProvider.js' diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index e4fb7aea..5961a6a7 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -4,13 +4,12 @@ import { LendProviderMorpho } from '@/lend/index.js' import { ChainManager } from '@/services/ChainManager.js' import type { LendProvider } from '@/types/lend.js' import type { VerbsConfig } from '@/types/verbs.js' - -import type { EmbeddedWalletProvider } from './wallet/providers/base/EmbeddedWalletProvider.js' -import type { SmartWalletProvider } from './wallet/providers/base/SmartWalletProvider.js' -import { DefaultSmartWalletProvider } from './wallet/providers/DefaultSmartWalletProvider.js' -import { PrivyEmbeddedWalletProvider } from './wallet/providers/privy.js' -import { WalletNamespace } from './wallet/WalletNamespace.js' -import { WalletProvider } from './wallet/WalletProvider.js' +import type { EmbeddedWalletProvider } from '@/wallet/providers/base/EmbeddedWalletProvider.js' +import type { SmartWalletProvider } from '@/wallet/providers/base/SmartWalletProvider.js' +import { DefaultSmartWalletProvider } from '@/wallet/providers/DefaultSmartWalletProvider.js' +import { PrivyEmbeddedWalletProvider } from '@/wallet/providers/PrivyEmbeddedWalletProvider.js' +import { WalletNamespace } from '@/wallet/WalletNamespace.js' +import { WalletProvider } from '@/wallet/WalletProvider.js' /** * Main Verbs SDK class diff --git a/packages/sdk/src/wallet/providers/privy.ts b/packages/sdk/src/wallet/providers/PrivyEmbeddedWalletProvider.ts similarity index 98% rename from packages/sdk/src/wallet/providers/privy.ts rename to packages/sdk/src/wallet/providers/PrivyEmbeddedWalletProvider.ts index 5d5b8601..999f7e80 100644 --- a/packages/sdk/src/wallet/providers/privy.ts +++ b/packages/sdk/src/wallet/providers/PrivyEmbeddedWalletProvider.ts @@ -6,8 +6,6 @@ import type { LendProvider } from '@/types/lend.js' import { PrivyWallet } from '@/wallet/PrivyWallet.js' import { EmbeddedWalletProvider } from '@/wallet/providers/base/EmbeddedWalletProvider.js' -// TODO: rename file to PrivyEmbeddedWalletProvider.ts - /** * Options for getting all wallets * @description Parameters for filtering and paginating wallet results From 3b9f1c5aab4690cb7b57d6c9941ea9214b8c2a40 Mon Sep 17 00:00:00 2001 From: tre Date: Thu, 28 Aug 2025 11:02:12 -0700 Subject: [PATCH 37/39] nit --- packages/sdk/src/lend/providers/morpho/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sdk/src/lend/providers/morpho/index.ts b/packages/sdk/src/lend/providers/morpho/index.ts index 5cd7bfe7..bca482ed 100644 --- a/packages/sdk/src/lend/providers/morpho/index.ts +++ b/packages/sdk/src/lend/providers/morpho/index.ts @@ -42,7 +42,6 @@ export const SUPPORTED_NETWORKS = { export class LendProviderMorpho extends LendProvider { protected readonly SUPPORTED_NETWORKS = SUPPORTED_NETWORKS - /** TODO: refactor. for now, this only supports Unichain */ private defaultSlippage: number private chainManager: ChainManager From b8d253633809fa629e8d40cf6a7c47aadbf00ea3 Mon Sep 17 00:00:00 2001 From: tre Date: Thu, 28 Aug 2025 11:04:48 -0700 Subject: [PATCH 38/39] add docs to chainmanager --- packages/sdk/src/services/ChainManager.ts | 44 +++++++++++++++++-- .../providers/base/SmartWalletProvider.ts | 2 +- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/services/ChainManager.ts b/packages/sdk/src/services/ChainManager.ts index 3657f230..d654766f 100644 --- a/packages/sdk/src/services/ChainManager.ts +++ b/packages/sdk/src/services/ChainManager.ts @@ -6,16 +6,21 @@ import { createBundlerClient } from 'viem/account-abstraction' import type { SUPPORTED_CHAIN_IDS } from '@/constants/supportedChains.js' import type { ChainConfig } from '@/types/chain.js' -// TODO: Add better documentation for this class once design is finalized - /** * Chain Manager Service - * @description Manages public clients and chain infrastructure for the Verbs SDK + * @description Manages public clients and chain infrastructure for the Verbs SDK. + * Provides utilities for accessing RPC and bundler URLs, and creating clients for supported chains. */ export class ChainManager { + /** Map of chain IDs to their corresponding public clients */ private publicClients: Map<(typeof SUPPORTED_CHAIN_IDS)[number], PublicClient> + /** Configuration for each supported chain */ private chainConfigs: ChainConfig[] + /** + * Initialize the ChainManager with chain configurations + * @param chains - Array of chain configurations + */ constructor(chains: ChainConfig[]) { this.chainConfigs = chains this.publicClients = this.createPublicClients(chains) @@ -23,6 +28,9 @@ export class ChainManager { /** * Get public client for a specific chain + * @param chainId - The chain ID to retrieve the public client for + * @returns PublicClient instance for the specified chain + * @throws Error if no client is configured for the chain ID */ getPublicClient(chainId: (typeof SUPPORTED_CHAIN_IDS)[number]): PublicClient { const client = this.publicClients.get(chainId) @@ -32,6 +40,13 @@ export class ChainManager { return client } + /** + * Get bundler client for a specific chain + * @param chainId - The chain ID to retrieve the bundler client for + * @param account - SmartAccount to use with the bundler client + * @returns BundlerClient instance for the specified chain + * @throws Error if no bundler URL is configured for the chain ID + */ getBundlerClient( chainId: (typeof SUPPORTED_CHAIN_IDS)[number], account: SmartAccount, @@ -52,6 +67,12 @@ export class ChainManager { }) } + /** + * Get RPC URL for a specific chain + * @param chainId - The chain ID to retrieve the RPC URL for + * @returns RPC URL as a string + * @throws Error if no chain config is found for the chain ID + */ getRpcUrl(chainId: (typeof SUPPORTED_CHAIN_IDS)[number]): string { const chainConfig = this.chainConfigs.find((c) => c.chainId === chainId) if (!chainConfig) { @@ -60,6 +81,12 @@ export class ChainManager { return chainConfig.rpcUrl } + /** + * Get bundler URL for a specific chain + * @param chainId - The chain ID to retrieve the bundler URL for + * @returns Bundler URL as a string or undefined if not configured + * @throws Error if no chain config is found for the chain ID + */ getBundlerUrl( chainId: (typeof SUPPORTED_CHAIN_IDS)[number], ): string | undefined { @@ -70,12 +97,18 @@ export class ChainManager { return chainConfig.bundlerUrl } + /** + * Get chain information for a specific chain ID + * @param chainId - The chain ID to retrieve information for + * @returns Chain object containing chain details + */ getChain(chainId: (typeof SUPPORTED_CHAIN_IDS)[number]): Chain { return chainById[chainId] } /** - * Get supported chain IDs + * Get all supported chain IDs + * @returns Array of supported chain IDs */ getSupportedChains() { return this.chainConfigs.map((c) => c.chainId) @@ -83,6 +116,9 @@ export class ChainManager { /** * Create public clients for all configured chains + * @param chains - Array of chain configurations + * @returns Map of chain IDs to their corresponding public clients + * @throws Error if a chain is not found or already configured */ private createPublicClients( chains: ChainConfig[], diff --git a/packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts b/packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts index 5fae17dd..b276c6ab 100644 --- a/packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts +++ b/packages/sdk/src/wallet/providers/base/SmartWalletProvider.ts @@ -5,7 +5,7 @@ import type { SmartWallet } from '@/wallet/base/SmartWallet.js' /** * Base smart wallet provider interface - * @description Abstract interface for smart wallet providers (Native, etc.) + * @description Abstract interface for smart wallet providers. */ export abstract class SmartWalletProvider { /** From a7213c3e87eb208fefb598a060d66040f7055c8a Mon Sep 17 00:00:00 2001 From: tre Date: Thu, 28 Aug 2025 11:09:27 -0700 Subject: [PATCH 39/39] rename --- .../sdk/src/wallet/{SmartWallet.ts => DefaultSmartWallet.ts} | 0 packages/sdk/src/wallet/providers/DefaultSmartWalletProvider.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/sdk/src/wallet/{SmartWallet.ts => DefaultSmartWallet.ts} (100%) diff --git a/packages/sdk/src/wallet/SmartWallet.ts b/packages/sdk/src/wallet/DefaultSmartWallet.ts similarity index 100% rename from packages/sdk/src/wallet/SmartWallet.ts rename to packages/sdk/src/wallet/DefaultSmartWallet.ts diff --git a/packages/sdk/src/wallet/providers/DefaultSmartWalletProvider.ts b/packages/sdk/src/wallet/providers/DefaultSmartWalletProvider.ts index 91de9720..735440a3 100644 --- a/packages/sdk/src/wallet/providers/DefaultSmartWalletProvider.ts +++ b/packages/sdk/src/wallet/providers/DefaultSmartWalletProvider.ts @@ -6,8 +6,8 @@ import { smartWalletFactoryAbi } from '@/abis/smartWalletFactory.js' import { smartWalletFactoryAddress } from '@/constants/addresses.js' import type { ChainManager } from '@/services/ChainManager.js' import type { LendProvider } from '@/types/lend.js' +import { DefaultSmartWallet } from '@/wallet/DefaultSmartWallet.js' import { SmartWalletProvider } from '@/wallet/providers/base/SmartWalletProvider.js' -import { DefaultSmartWallet } from '@/wallet/SmartWallet.js' /** * Smart Wallet Provider