From 46415c9f8e86c90aa9998ac2fad0afb2a67dc40f Mon Sep 17 00:00:00 2001
From: Daniel Schlabach <31226559+dschlabach@users.noreply.github.com>
Date: Thu, 23 Jan 2025 18:05:00 -0500
Subject: [PATCH] feat: add hooks for preparing Morpho transactions (#1871)
---
src/earn/abis/morpho.ts | 1400 +++++++++++++++++
src/earn/constants.ts | 24 -
.../hooks/useBuildMorphoDepositTx.test.ts | 59 +
src/earn/hooks/useBuildMorphoDepositTx.ts | 46 +
.../hooks/useBuildMorphoWithdrawTx.test.ts | 71 +
src/earn/hooks/useBuildMorphoWithdrawTx.ts | 53 +
src/earn/hooks/useMorphoVault.test.ts | 90 ++
src/earn/hooks/useMorphoVault.ts | 68 +
src/earn/utils/buildDepositToMorphoTx.test.ts | 7 +-
src/earn/utils/buildDepositToMorphoTx.ts | 2 +-
.../utils/buildWithdrawFromMorphoTx.test.ts | 3 +-
src/earn/utils/buildWithdrawFromMorphoTx.ts | 6 +-
12 files changed, 1795 insertions(+), 34 deletions(-)
create mode 100644 src/earn/abis/morpho.ts
create mode 100644 src/earn/hooks/useBuildMorphoDepositTx.test.ts
create mode 100644 src/earn/hooks/useBuildMorphoDepositTx.ts
create mode 100644 src/earn/hooks/useBuildMorphoWithdrawTx.test.ts
create mode 100644 src/earn/hooks/useBuildMorphoWithdrawTx.ts
create mode 100644 src/earn/hooks/useMorphoVault.test.ts
create mode 100644 src/earn/hooks/useMorphoVault.ts
diff --git a/src/earn/abis/morpho.ts b/src/earn/abis/morpho.ts
new file mode 100644
index 0000000000..8c0860f8a3
--- /dev/null
+++ b/src/earn/abis/morpho.ts
@@ -0,0 +1,1400 @@
+export const MORPHO_VAULT_ABI = [
+ {
+ inputs: [
+ { internalType: 'address', name: 'owner', type: 'address' },
+ { internalType: 'address', name: 'morpho', type: 'address' },
+ { internalType: 'uint256', name: 'initialTimelock', type: 'uint256' },
+ { internalType: 'address', name: '_asset', type: 'address' },
+ { internalType: 'string', name: '_name', type: 'string' },
+ { internalType: 'string', name: '_symbol', type: 'string' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'constructor',
+ },
+ { inputs: [], name: 'AboveMaxTimelock', type: 'error' },
+ {
+ inputs: [{ internalType: 'address', name: 'target', type: 'address' }],
+ name: 'AddressEmptyCode',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
+ name: 'AddressInsufficientBalance',
+ type: 'error',
+ },
+ { inputs: [], name: 'AllCapsReached', type: 'error' },
+ { inputs: [], name: 'AlreadyPending', type: 'error' },
+ { inputs: [], name: 'AlreadySet', type: 'error' },
+ { inputs: [], name: 'BelowMinTimelock', type: 'error' },
+ {
+ inputs: [{ internalType: 'Id', name: 'id', type: 'bytes32' }],
+ name: 'DuplicateMarket',
+ type: 'error',
+ },
+ { inputs: [], name: 'ECDSAInvalidSignature', type: 'error' },
+ {
+ inputs: [{ internalType: 'uint256', name: 'length', type: 'uint256' }],
+ name: 'ECDSAInvalidSignatureLength',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'bytes32', name: 's', type: 'bytes32' }],
+ name: 'ECDSAInvalidSignatureS',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'spender', type: 'address' },
+ { internalType: 'uint256', name: 'allowance', type: 'uint256' },
+ { internalType: 'uint256', name: 'needed', type: 'uint256' },
+ ],
+ name: 'ERC20InsufficientAllowance',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'sender', type: 'address' },
+ { internalType: 'uint256', name: 'balance', type: 'uint256' },
+ { internalType: 'uint256', name: 'needed', type: 'uint256' },
+ ],
+ name: 'ERC20InsufficientBalance',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'approver', type: 'address' }],
+ name: 'ERC20InvalidApprover',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'receiver', type: 'address' }],
+ name: 'ERC20InvalidReceiver',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'sender', type: 'address' }],
+ name: 'ERC20InvalidSender',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'spender', type: 'address' }],
+ name: 'ERC20InvalidSpender',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'deadline', type: 'uint256' }],
+ name: 'ERC2612ExpiredSignature',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'signer', type: 'address' },
+ { internalType: 'address', name: 'owner', type: 'address' },
+ ],
+ name: 'ERC2612InvalidSigner',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'receiver', type: 'address' },
+ { internalType: 'uint256', name: 'assets', type: 'uint256' },
+ { internalType: 'uint256', name: 'max', type: 'uint256' },
+ ],
+ name: 'ERC4626ExceededMaxDeposit',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'receiver', type: 'address' },
+ { internalType: 'uint256', name: 'shares', type: 'uint256' },
+ { internalType: 'uint256', name: 'max', type: 'uint256' },
+ ],
+ name: 'ERC4626ExceededMaxMint',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'owner', type: 'address' },
+ { internalType: 'uint256', name: 'shares', type: 'uint256' },
+ { internalType: 'uint256', name: 'max', type: 'uint256' },
+ ],
+ name: 'ERC4626ExceededMaxRedeem',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'owner', type: 'address' },
+ { internalType: 'uint256', name: 'assets', type: 'uint256' },
+ { internalType: 'uint256', name: 'max', type: 'uint256' },
+ ],
+ name: 'ERC4626ExceededMaxWithdraw',
+ type: 'error',
+ },
+ { inputs: [], name: 'FailedInnerCall', type: 'error' },
+ {
+ inputs: [{ internalType: 'Id', name: 'id', type: 'bytes32' }],
+ name: 'InconsistentAsset',
+ type: 'error',
+ },
+ { inputs: [], name: 'InconsistentReallocation', type: 'error' },
+ {
+ inputs: [
+ { internalType: 'address', name: 'account', type: 'address' },
+ { internalType: 'uint256', name: 'currentNonce', type: 'uint256' },
+ ],
+ name: 'InvalidAccountNonce',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'Id', name: 'id', type: 'bytes32' }],
+ name: 'InvalidMarketRemovalNonZeroCap',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'Id', name: 'id', type: 'bytes32' }],
+ name: 'InvalidMarketRemovalNonZeroSupply',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'Id', name: 'id', type: 'bytes32' }],
+ name: 'InvalidMarketRemovalTimelockNotElapsed',
+ type: 'error',
+ },
+ { inputs: [], name: 'InvalidShortString', type: 'error' },
+ { inputs: [], name: 'MarketNotCreated', type: 'error' },
+ {
+ inputs: [{ internalType: 'Id', name: 'id', type: 'bytes32' }],
+ name: 'MarketNotEnabled',
+ type: 'error',
+ },
+ { inputs: [], name: 'MathOverflowedMulDiv', type: 'error' },
+ { inputs: [], name: 'MaxFeeExceeded', type: 'error' },
+ { inputs: [], name: 'MaxQueueLengthExceeded', type: 'error' },
+ { inputs: [], name: 'NoPendingValue', type: 'error' },
+ { inputs: [], name: 'NonZeroCap', type: 'error' },
+ { inputs: [], name: 'NotAllocatorRole', type: 'error' },
+ { inputs: [], name: 'NotCuratorNorGuardianRole', type: 'error' },
+ { inputs: [], name: 'NotCuratorRole', type: 'error' },
+ { inputs: [], name: 'NotEnoughLiquidity', type: 'error' },
+ { inputs: [], name: 'NotGuardianRole', type: 'error' },
+ {
+ inputs: [{ internalType: 'address', name: 'owner', type: 'address' }],
+ name: 'OwnableInvalidOwner',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
+ name: 'OwnableUnauthorizedAccount',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'Id', name: 'id', type: 'bytes32' }],
+ name: 'PendingCap',
+ type: 'error',
+ },
+ { inputs: [], name: 'PendingRemoval', type: 'error' },
+ {
+ inputs: [
+ { internalType: 'uint8', name: 'bits', type: 'uint8' },
+ { internalType: 'uint256', name: 'value', type: 'uint256' },
+ ],
+ name: 'SafeCastOverflowedUintDowncast',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'token', type: 'address' }],
+ name: 'SafeERC20FailedOperation',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'string', name: 'str', type: 'string' }],
+ name: 'StringTooLong',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'Id', name: 'id', type: 'bytes32' }],
+ name: 'SupplyCapExceeded',
+ type: 'error',
+ },
+ { inputs: [], name: 'TimelockNotElapsed', type: 'error' },
+ {
+ inputs: [{ internalType: 'Id', name: 'id', type: 'bytes32' }],
+ name: 'UnauthorizedMarket',
+ type: 'error',
+ },
+ { inputs: [], name: 'ZeroAddress', type: 'error' },
+ { inputs: [], name: 'ZeroFeeRecipient', type: 'error' },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'newTotalAssets',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'feeShares',
+ type: 'uint256',
+ },
+ ],
+ name: 'AccrueInterest',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'owner',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'spender',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'value',
+ type: 'uint256',
+ },
+ ],
+ name: 'Approval',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'sender',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'owner',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'assets',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'shares',
+ type: 'uint256',
+ },
+ ],
+ name: 'Deposit',
+ type: 'event',
+ },
+ { anonymous: false, inputs: [], name: 'EIP712DomainChanged', type: 'event' },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'previousOwner',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'newOwner',
+ type: 'address',
+ },
+ ],
+ name: 'OwnershipTransferStarted',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'previousOwner',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'newOwner',
+ type: 'address',
+ },
+ ],
+ name: 'OwnershipTransferred',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ { indexed: true, internalType: 'Id', name: 'id', type: 'bytes32' },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'suppliedAssets',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'suppliedShares',
+ type: 'uint256',
+ },
+ ],
+ name: 'ReallocateSupply',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ { indexed: true, internalType: 'Id', name: 'id', type: 'bytes32' },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'withdrawnAssets',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'withdrawnShares',
+ type: 'uint256',
+ },
+ ],
+ name: 'ReallocateWithdraw',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ { indexed: true, internalType: 'Id', name: 'id', type: 'bytes32' },
+ ],
+ name: 'RevokePendingCap',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ ],
+ name: 'RevokePendingGuardian',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ { indexed: true, internalType: 'Id', name: 'id', type: 'bytes32' },
+ ],
+ name: 'RevokePendingMarketRemoval',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ ],
+ name: 'RevokePendingTimelock',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ { indexed: true, internalType: 'Id', name: 'id', type: 'bytes32' },
+ { indexed: false, internalType: 'uint256', name: 'cap', type: 'uint256' },
+ ],
+ name: 'SetCap',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'newCurator',
+ type: 'address',
+ },
+ ],
+ name: 'SetCurator',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'newFee',
+ type: 'uint256',
+ },
+ ],
+ name: 'SetFee',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'newFeeRecipient',
+ type: 'address',
+ },
+ ],
+ name: 'SetFeeRecipient',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'guardian',
+ type: 'address',
+ },
+ ],
+ name: 'SetGuardian',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'allocator',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'bool',
+ name: 'isAllocator',
+ type: 'bool',
+ },
+ ],
+ name: 'SetIsAllocator',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'newSkimRecipient',
+ type: 'address',
+ },
+ ],
+ name: 'SetSkimRecipient',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'Id[]',
+ name: 'newSupplyQueue',
+ type: 'bytes32[]',
+ },
+ ],
+ name: 'SetSupplyQueue',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'newTimelock',
+ type: 'uint256',
+ },
+ ],
+ name: 'SetTimelock',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'Id[]',
+ name: 'newWithdrawQueue',
+ type: 'bytes32[]',
+ },
+ ],
+ name: 'SetWithdrawQueue',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'token',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'amount',
+ type: 'uint256',
+ },
+ ],
+ name: 'Skim',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ { indexed: true, internalType: 'Id', name: 'id', type: 'bytes32' },
+ { indexed: false, internalType: 'uint256', name: 'cap', type: 'uint256' },
+ ],
+ name: 'SubmitCap',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'newGuardian',
+ type: 'address',
+ },
+ ],
+ name: 'SubmitGuardian',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'caller',
+ type: 'address',
+ },
+ { indexed: true, internalType: 'Id', name: 'id', type: 'bytes32' },
+ ],
+ name: 'SubmitMarketRemoval',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'newTimelock',
+ type: 'uint256',
+ },
+ ],
+ name: 'SubmitTimelock',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'from', type: 'address' },
+ { indexed: true, internalType: 'address', name: 'to', type: 'address' },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'value',
+ type: 'uint256',
+ },
+ ],
+ name: 'Transfer',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'updatedTotalAssets',
+ type: 'uint256',
+ },
+ ],
+ name: 'UpdateLastTotalAssets',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'sender',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'receiver',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'owner',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'assets',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'shares',
+ type: 'uint256',
+ },
+ ],
+ name: 'Withdraw',
+ type: 'event',
+ },
+ {
+ inputs: [],
+ name: 'DECIMALS_OFFSET',
+ outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'DOMAIN_SEPARATOR',
+ outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'MORPHO',
+ outputs: [{ internalType: 'contract IMorpho', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'address', name: 'loanToken', type: 'address' },
+ { internalType: 'address', name: 'collateralToken', type: 'address' },
+ { internalType: 'address', name: 'oracle', type: 'address' },
+ { internalType: 'address', name: 'irm', type: 'address' },
+ { internalType: 'uint256', name: 'lltv', type: 'uint256' },
+ ],
+ internalType: 'struct MarketParams',
+ name: 'marketParams',
+ type: 'tuple',
+ },
+ ],
+ name: 'acceptCap',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'acceptGuardian',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'acceptOwnership',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'acceptTimelock',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'owner', type: 'address' },
+ { internalType: 'address', name: 'spender', type: 'address' },
+ ],
+ name: 'allowance',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'spender', type: 'address' },
+ { internalType: 'uint256', name: 'value', type: 'uint256' },
+ ],
+ name: 'approve',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'asset',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
+ name: 'balanceOf',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'Id', name: '', type: 'bytes32' }],
+ name: 'config',
+ outputs: [
+ { internalType: 'uint184', name: 'cap', type: 'uint184' },
+ { internalType: 'bool', name: 'enabled', type: 'bool' },
+ { internalType: 'uint64', name: 'removableAt', type: 'uint64' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'shares', type: 'uint256' }],
+ name: 'convertToAssets',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }],
+ name: 'convertToShares',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'curator',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'decimals',
+ outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: 'assets', type: 'uint256' },
+ { internalType: 'address', name: 'receiver', type: 'address' },
+ ],
+ name: 'deposit',
+ outputs: [{ internalType: 'uint256', name: 'shares', type: 'uint256' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'eip712Domain',
+ outputs: [
+ { internalType: 'bytes1', name: 'fields', type: 'bytes1' },
+ { internalType: 'string', name: 'name', type: 'string' },
+ { internalType: 'string', name: 'version', type: 'string' },
+ { internalType: 'uint256', name: 'chainId', type: 'uint256' },
+ { internalType: 'address', name: 'verifyingContract', type: 'address' },
+ { internalType: 'bytes32', name: 'salt', type: 'bytes32' },
+ { internalType: 'uint256[]', name: 'extensions', type: 'uint256[]' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'fee',
+ outputs: [{ internalType: 'uint96', name: '', type: 'uint96' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'feeRecipient',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'guardian',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: '', type: 'address' }],
+ name: 'isAllocator',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'lastTotalAssets',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: '', type: 'address' }],
+ name: 'maxDeposit',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: '', type: 'address' }],
+ name: 'maxMint',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'owner', type: 'address' }],
+ name: 'maxRedeem',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'owner', type: 'address' }],
+ name: 'maxWithdraw',
+ outputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: 'shares', type: 'uint256' },
+ { internalType: 'address', name: 'receiver', type: 'address' },
+ ],
+ name: 'mint',
+ outputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes[]', name: 'data', type: 'bytes[]' }],
+ name: 'multicall',
+ outputs: [{ internalType: 'bytes[]', name: 'results', type: 'bytes[]' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'name',
+ outputs: [{ internalType: 'string', name: '', type: 'string' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'owner', type: 'address' }],
+ name: 'nonces',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'owner',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'Id', name: '', type: 'bytes32' }],
+ name: 'pendingCap',
+ outputs: [
+ { internalType: 'uint192', name: 'value', type: 'uint192' },
+ { internalType: 'uint64', name: 'validAt', type: 'uint64' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'pendingGuardian',
+ outputs: [
+ { internalType: 'address', name: 'value', type: 'address' },
+ { internalType: 'uint64', name: 'validAt', type: 'uint64' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'pendingOwner',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'pendingTimelock',
+ outputs: [
+ { internalType: 'uint192', name: 'value', type: 'uint192' },
+ { internalType: 'uint64', name: 'validAt', type: 'uint64' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'owner', type: 'address' },
+ { internalType: 'address', name: 'spender', type: 'address' },
+ { internalType: 'uint256', name: 'value', type: 'uint256' },
+ { internalType: 'uint256', name: 'deadline', type: 'uint256' },
+ { internalType: 'uint8', name: 'v', type: 'uint8' },
+ { internalType: 'bytes32', name: 'r', type: 'bytes32' },
+ { internalType: 'bytes32', name: 's', type: 'bytes32' },
+ ],
+ name: 'permit',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }],
+ name: 'previewDeposit',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'shares', type: 'uint256' }],
+ name: 'previewMint',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'shares', type: 'uint256' }],
+ name: 'previewRedeem',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }],
+ name: 'previewWithdraw',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ {
+ components: [
+ { internalType: 'address', name: 'loanToken', type: 'address' },
+ {
+ internalType: 'address',
+ name: 'collateralToken',
+ type: 'address',
+ },
+ { internalType: 'address', name: 'oracle', type: 'address' },
+ { internalType: 'address', name: 'irm', type: 'address' },
+ { internalType: 'uint256', name: 'lltv', type: 'uint256' },
+ ],
+ internalType: 'struct MarketParams',
+ name: 'marketParams',
+ type: 'tuple',
+ },
+ { internalType: 'uint256', name: 'assets', type: 'uint256' },
+ ],
+ internalType: 'struct MarketAllocation[]',
+ name: 'allocations',
+ type: 'tuple[]',
+ },
+ ],
+ name: 'reallocate',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: 'shares', type: 'uint256' },
+ { internalType: 'address', name: 'receiver', type: 'address' },
+ { internalType: 'address', name: 'owner', type: 'address' },
+ ],
+ name: 'redeem',
+ outputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'renounceOwnership',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'Id', name: 'id', type: 'bytes32' }],
+ name: 'revokePendingCap',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'revokePendingGuardian',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'Id', name: 'id', type: 'bytes32' }],
+ name: 'revokePendingMarketRemoval',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'revokePendingTimelock',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'newCurator', type: 'address' }],
+ name: 'setCurator',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'newFee', type: 'uint256' }],
+ name: 'setFee',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'newFeeRecipient', type: 'address' },
+ ],
+ name: 'setFeeRecipient',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'newAllocator', type: 'address' },
+ { internalType: 'bool', name: 'newIsAllocator', type: 'bool' },
+ ],
+ name: 'setIsAllocator',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'newSkimRecipient', type: 'address' },
+ ],
+ name: 'setSkimRecipient',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'Id[]', name: 'newSupplyQueue', type: 'bytes32[]' },
+ ],
+ name: 'setSupplyQueue',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'token', type: 'address' }],
+ name: 'skim',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'skimRecipient',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'address', name: 'loanToken', type: 'address' },
+ { internalType: 'address', name: 'collateralToken', type: 'address' },
+ { internalType: 'address', name: 'oracle', type: 'address' },
+ { internalType: 'address', name: 'irm', type: 'address' },
+ { internalType: 'uint256', name: 'lltv', type: 'uint256' },
+ ],
+ internalType: 'struct MarketParams',
+ name: 'marketParams',
+ type: 'tuple',
+ },
+ { internalType: 'uint256', name: 'newSupplyCap', type: 'uint256' },
+ ],
+ name: 'submitCap',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'newGuardian', type: 'address' }],
+ name: 'submitGuardian',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'address', name: 'loanToken', type: 'address' },
+ { internalType: 'address', name: 'collateralToken', type: 'address' },
+ { internalType: 'address', name: 'oracle', type: 'address' },
+ { internalType: 'address', name: 'irm', type: 'address' },
+ { internalType: 'uint256', name: 'lltv', type: 'uint256' },
+ ],
+ internalType: 'struct MarketParams',
+ name: 'marketParams',
+ type: 'tuple',
+ },
+ ],
+ name: 'submitMarketRemoval',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'newTimelock', type: 'uint256' }],
+ name: 'submitTimelock',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ name: 'supplyQueue',
+ outputs: [{ internalType: 'Id', name: '', type: 'bytes32' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'supplyQueueLength',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'symbol',
+ outputs: [{ internalType: 'string', name: '', type: 'string' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'timelock',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'totalAssets',
+ outputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'totalSupply',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'to', type: 'address' },
+ { internalType: 'uint256', name: 'value', type: 'uint256' },
+ ],
+ name: 'transfer',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'from', type: 'address' },
+ { internalType: 'address', name: 'to', type: 'address' },
+ { internalType: 'uint256', name: 'value', type: 'uint256' },
+ ],
+ name: 'transferFrom',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
+ name: 'transferOwnership',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256[]', name: 'indexes', type: 'uint256[]' }],
+ name: 'updateWithdrawQueue',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: 'assets', type: 'uint256' },
+ { internalType: 'address', name: 'receiver', type: 'address' },
+ { internalType: 'address', name: 'owner', type: 'address' },
+ ],
+ name: 'withdraw',
+ outputs: [{ internalType: 'uint256', name: 'shares', type: 'uint256' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ name: 'withdrawQueue',
+ outputs: [{ internalType: 'Id', name: '', type: 'bytes32' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'withdrawQueueLength',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+] as const;
diff --git a/src/earn/constants.ts b/src/earn/constants.ts
index b57e488c2c..3ae9cf0958 100644
--- a/src/earn/constants.ts
+++ b/src/earn/constants.ts
@@ -1,26 +1,2 @@
-export const MORPHO_VAULT_ABI = [
- {
- inputs: [
- { internalType: 'uint256', name: 'assets', type: 'uint256' },
- { internalType: 'address', name: 'receiver', type: 'address' },
- ],
- name: 'deposit',
- outputs: [{ internalType: 'uint256', name: 'shares', type: 'uint256' }],
- stateMutability: 'nonpayable',
- type: 'function',
- },
- {
- inputs: [
- { internalType: 'uint256', name: 'assets', type: 'uint256' },
- { internalType: 'address', name: 'receiver', type: 'address' },
- { internalType: 'address', name: 'owner', type: 'address' },
- ],
- name: 'withdraw',
- outputs: [{ internalType: 'uint256', name: 'shares', type: 'uint256' }],
- stateMutability: 'nonpayable',
- type: 'function',
- },
-] as const;
-
export const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
export const USDC_DECIMALS = 6;
diff --git a/src/earn/hooks/useBuildMorphoDepositTx.test.ts b/src/earn/hooks/useBuildMorphoDepositTx.test.ts
new file mode 100644
index 0000000000..ca49b70be3
--- /dev/null
+++ b/src/earn/hooks/useBuildMorphoDepositTx.test.ts
@@ -0,0 +1,59 @@
+import { renderHook } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import {
+ type UseBuildMorphoDepositTxParams,
+ useBuildMorphoDepositTx,
+} from './useBuildMorphoDepositTx';
+import { useMorphoVault } from './useMorphoVault';
+
+const DUMMY_ADDRESS = '0x9E95f497a7663B70404496dB6481c890C4825fe1' as const;
+
+// Mock dependencies
+vi.mock('./useMorphoVault');
+vi.mock('@/earn/utils/buildDepositToMorphoTx', () => ({
+ buildDepositToMorphoTx: vi
+ .fn()
+ .mockReturnValue([{ to: '0x123', data: '0x456' }]),
+}));
+
+describe('useBuildMorphoDepositTx', () => {
+ const mockParams: UseBuildMorphoDepositTxParams = {
+ vaultAddress: DUMMY_ADDRESS,
+ receiverAddress: DUMMY_ADDRESS,
+ amount: 100,
+ };
+
+ it('returns empty calls when vault data is not available', () => {
+ vi.mocked(useMorphoVault).mockReturnValue({
+ status: 'pending',
+ asset: undefined,
+ balance: undefined,
+ assetDecimals: undefined,
+ vaultDecimals: undefined,
+ name: undefined,
+ });
+
+ const { result } = renderHook(() => useBuildMorphoDepositTx(mockParams));
+
+ expect(result.current.calls).toEqual([]);
+ });
+
+ it('builds deposit transaction when vault data is available', () => {
+ const mockAsset = DUMMY_ADDRESS;
+ const mockDecimals = 18;
+
+ vi.mocked(useMorphoVault).mockReturnValue({
+ status: 'success',
+ asset: mockAsset,
+ balance: '1000',
+ assetDecimals: mockDecimals,
+ vaultDecimals: mockDecimals,
+ name: 'Mock Name',
+ });
+
+ const { result } = renderHook(() => useBuildMorphoDepositTx(mockParams));
+
+ expect(result.current.calls).toEqual([{ to: '0x123', data: '0x456' }]);
+ expect(result.current.calls).toHaveLength(1);
+ });
+});
diff --git a/src/earn/hooks/useBuildMorphoDepositTx.ts b/src/earn/hooks/useBuildMorphoDepositTx.ts
new file mode 100644
index 0000000000..52262ce4ea
--- /dev/null
+++ b/src/earn/hooks/useBuildMorphoDepositTx.ts
@@ -0,0 +1,46 @@
+import { useMorphoVault } from '@/earn/hooks/useMorphoVault';
+import { buildDepositToMorphoTx } from '@/earn/utils/buildDepositToMorphoTx';
+import type { Call } from '@/transaction/types';
+import { type Address, parseUnits } from 'viem';
+
+export type UseBuildMorphoDepositTxParams = {
+ vaultAddress: Address;
+ receiverAddress: Address;
+ amount: number;
+};
+
+/**
+ * Generates Call[] for a Morpho deposit transaction
+ * to be used with
+ */
+export function useBuildMorphoDepositTx({
+ vaultAddress,
+ receiverAddress,
+ amount,
+}: UseBuildMorphoDepositTxParams): {
+ calls: Call[];
+} {
+ const { asset, balance, assetDecimals } = useMorphoVault({
+ vaultAddress,
+ address: receiverAddress,
+ });
+
+ if (!asset || balance === undefined || !assetDecimals) {
+ return {
+ calls: [],
+ };
+ }
+
+ const parsedAmount = parseUnits(amount.toString(), assetDecimals);
+
+ const calls = buildDepositToMorphoTx({
+ receiverAddress,
+ vaultAddress,
+ tokenAddress: asset,
+ amount: parsedAmount,
+ });
+
+ return {
+ calls,
+ };
+}
diff --git a/src/earn/hooks/useBuildMorphoWithdrawTx.test.ts b/src/earn/hooks/useBuildMorphoWithdrawTx.test.ts
new file mode 100644
index 0000000000..562b1135e3
--- /dev/null
+++ b/src/earn/hooks/useBuildMorphoWithdrawTx.test.ts
@@ -0,0 +1,71 @@
+import { renderHook } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import {
+ type UseBuildMorphoWithdrawTxParams,
+ useBuildMorphoWithdrawTx,
+} from './useBuildMorphoWithdrawTx';
+import { useMorphoVault } from './useMorphoVault';
+
+const DUMMY_ADDRESS = '0x9E95f497a7663B70404496dB6481c890C4825fe1' as const;
+
+// Mock dependencies
+vi.mock('./useMorphoVault');
+vi.mock('@/earn/utils/buildWithdrawFromMorphoTx', () => ({
+ buildWithdrawFromMorphoTx: vi
+ .fn()
+ .mockReturnValue([{ to: '0x123', data: '0x456' }]),
+}));
+
+describe('useBuildMorphoWithdrawTx', () => {
+ const mockParams: UseBuildMorphoWithdrawTxParams = {
+ vaultAddress: DUMMY_ADDRESS,
+ receiverAddress: DUMMY_ADDRESS,
+ amount: 100,
+ };
+
+ it('returns empty calls when vault data is not available', () => {
+ vi.mocked(useMorphoVault).mockReturnValue({
+ status: 'pending',
+ asset: undefined,
+ balance: undefined,
+ assetDecimals: undefined,
+ vaultDecimals: undefined,
+ name: undefined,
+ });
+
+ const { result } = renderHook(() => useBuildMorphoWithdrawTx(mockParams));
+
+ expect(result.current.calls).toEqual([]);
+ });
+
+ it('returns empty calls when amount is greater than balance', () => {
+ vi.mocked(useMorphoVault).mockReturnValue({
+ status: 'success',
+ asset: DUMMY_ADDRESS,
+ balance: '50',
+ assetDecimals: 18,
+ vaultDecimals: 18,
+ name: 'Mock Name',
+ });
+
+ const { result } = renderHook(() => useBuildMorphoWithdrawTx(mockParams));
+
+ expect(result.current.calls).toEqual([]);
+ });
+
+ it('builds withdraw transaction when vault data is available and amount is valid', () => {
+ vi.mocked(useMorphoVault).mockReturnValue({
+ status: 'success',
+ asset: DUMMY_ADDRESS,
+ balance: '1000',
+ assetDecimals: 18,
+ vaultDecimals: 18,
+ name: 'Mock Name',
+ });
+
+ const { result } = renderHook(() => useBuildMorphoWithdrawTx(mockParams));
+
+ expect(result.current.calls).toEqual([{ to: '0x123', data: '0x456' }]);
+ expect(result.current.calls).toHaveLength(1);
+ });
+});
diff --git a/src/earn/hooks/useBuildMorphoWithdrawTx.ts b/src/earn/hooks/useBuildMorphoWithdrawTx.ts
new file mode 100644
index 0000000000..ec0c51dc2e
--- /dev/null
+++ b/src/earn/hooks/useBuildMorphoWithdrawTx.ts
@@ -0,0 +1,53 @@
+import { useMorphoVault } from '@/earn/hooks/useMorphoVault';
+import { buildWithdrawFromMorphoTx } from '@/earn/utils/buildWithdrawFromMorphoTx';
+import type { Call } from '@/transaction/types';
+import { type Address, parseUnits } from 'viem';
+
+export type UseBuildMorphoWithdrawTxParams = {
+ vaultAddress: Address;
+ receiverAddress: Address;
+ amount: number;
+};
+
+/**
+ * Generates Call[] for a Morpho withdraw transaction
+ * to be used with
+ */
+export function useBuildMorphoWithdrawTx({
+ vaultAddress,
+ amount,
+ receiverAddress,
+}: UseBuildMorphoWithdrawTxParams): {
+ calls: Call[];
+} {
+ const { asset, balance, assetDecimals, vaultDecimals } = useMorphoVault({
+ vaultAddress,
+ address: receiverAddress,
+ });
+
+ const amountIsGreaterThanBalance = amount > Number(balance);
+
+ if (
+ !asset ||
+ balance === undefined ||
+ !assetDecimals ||
+ !vaultDecimals ||
+ amountIsGreaterThanBalance
+ ) {
+ return {
+ calls: [],
+ };
+ }
+
+ const parsedAmount = parseUnits(amount.toString(), assetDecimals);
+
+ const calls = buildWithdrawFromMorphoTx({
+ receiverAddress,
+ vaultAddress,
+ amount: parsedAmount,
+ });
+
+ return {
+ calls,
+ };
+}
diff --git a/src/earn/hooks/useMorphoVault.test.ts b/src/earn/hooks/useMorphoVault.test.ts
new file mode 100644
index 0000000000..c597dc23bb
--- /dev/null
+++ b/src/earn/hooks/useMorphoVault.test.ts
@@ -0,0 +1,90 @@
+import { renderHook } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import {
+ type UseReadContractReturnType,
+ type UseReadContractsReturnType,
+ useReadContract,
+ useReadContracts,
+} from 'wagmi';
+import { useMorphoVault } from './useMorphoVault';
+
+const DUMMY_ADDRESS = '0x9E95f497a7663B70404496dB6481c890C4825fe1' as const;
+
+// Mock dependencies
+vi.mock('wagmi', () => ({
+ useReadContract: vi.fn(),
+ useReadContracts: vi.fn(),
+}));
+
+describe('useMorphoVault', () => {
+ const mockParams = {
+ vaultAddress: DUMMY_ADDRESS,
+ address: DUMMY_ADDRESS,
+ };
+
+ it('returns undefined values when contract reads are pending', () => {
+ vi.mocked(useReadContracts).mockReturnValue({
+ data: undefined,
+ status: 'pending',
+ } as UseReadContractsReturnType); // for brevity
+ vi.mocked(useReadContract).mockReturnValue({
+ data: undefined,
+ } as UseReadContractReturnType); // for brevity
+
+ const { result } = renderHook(() => useMorphoVault(mockParams));
+
+ expect(result.current).toEqual({
+ status: 'pending',
+ asset: undefined,
+ assetDecimals: undefined,
+ vaultDecimals: undefined,
+ name: undefined,
+ balance: undefined,
+ });
+ });
+
+ it('returns formatted data when contract reads are successful', () => {
+ vi.mocked(useReadContracts).mockReturnValue({
+ data: [
+ { result: DUMMY_ADDRESS }, // asset
+ { result: 'Morpho Vault' }, // name
+ { result: 1000000000000000000n }, // balanceOf
+ { result: 18 }, // decimals
+ ],
+ status: 'success',
+ } as UseReadContractsReturnType); // for brevity
+ vi.mocked(useReadContract).mockReturnValue({
+ data: 18,
+ } as UseReadContractReturnType); // for brevity
+
+ const { result } = renderHook(() => useMorphoVault(mockParams));
+
+ expect(result.current).toEqual({
+ status: 'success',
+ asset: DUMMY_ADDRESS,
+ assetDecimals: 18,
+ vaultDecimals: 18,
+ name: 'Morpho Vault',
+ balance: '1',
+ });
+ });
+
+ it('handles missing balance data', () => {
+ vi.mocked(useReadContracts).mockReturnValue({
+ data: [
+ { result: DUMMY_ADDRESS },
+ { result: 'Morpho Vault' },
+ { result: undefined }, // missing balance
+ { result: 18 },
+ ],
+ status: 'success',
+ } as UseReadContractsReturnType); // for brevity
+ vi.mocked(useReadContract).mockReturnValue({
+ data: 18,
+ } as UseReadContractReturnType); // for brevity
+
+ const { result } = renderHook(() => useMorphoVault(mockParams));
+
+ expect(result.current.balance).toBeUndefined();
+ });
+});
diff --git a/src/earn/hooks/useMorphoVault.ts b/src/earn/hooks/useMorphoVault.ts
new file mode 100644
index 0000000000..2b7c56a059
--- /dev/null
+++ b/src/earn/hooks/useMorphoVault.ts
@@ -0,0 +1,68 @@
+import { MORPHO_VAULT_ABI } from '@/earn/abis/morpho';
+import { type Address, erc20Abi, formatUnits } from 'viem';
+import { useReadContract, useReadContracts } from 'wagmi';
+
+type UseMorphoVaultParams = {
+ vaultAddress: Address;
+ address: Address;
+};
+
+export type UseMorphoVaultReturnType = {
+ status: 'pending' | 'success' | 'error';
+ asset: Address | undefined;
+ assetDecimals: number | undefined;
+ vaultDecimals: number | undefined;
+ name: string | undefined;
+ balance: string | undefined;
+};
+
+export function useMorphoVault({
+ vaultAddress,
+ address,
+}: UseMorphoVaultParams): UseMorphoVaultReturnType {
+ const { data, status } = useReadContracts({
+ contracts: [
+ {
+ abi: MORPHO_VAULT_ABI,
+ address: vaultAddress,
+ functionName: 'asset',
+ },
+ {
+ abi: MORPHO_VAULT_ABI,
+ address: vaultAddress,
+ functionName: 'name',
+ },
+ {
+ abi: MORPHO_VAULT_ABI,
+ address: vaultAddress,
+ functionName: 'balanceOf',
+ args: [address],
+ },
+ {
+ abi: MORPHO_VAULT_ABI,
+ address: vaultAddress,
+ functionName: 'decimals',
+ },
+ ],
+ });
+
+ const { data: tokenDecimals } = useReadContract({
+ abi: erc20Abi,
+ address: data?.[0].result,
+ functionName: 'decimals',
+ });
+
+ const formattedBalance =
+ data?.[2].result && data?.[3].result
+ ? formatUnits(data?.[2].result, data?.[3].result)
+ : undefined;
+
+ return {
+ status,
+ asset: data?.[0].result,
+ assetDecimals: tokenDecimals,
+ vaultDecimals: data?.[3].result,
+ name: data?.[1].result,
+ balance: formattedBalance,
+ };
+}
diff --git a/src/earn/utils/buildDepositToMorphoTx.test.ts b/src/earn/utils/buildDepositToMorphoTx.test.ts
index 15698b43d0..0ebd4d9825 100644
--- a/src/earn/utils/buildDepositToMorphoTx.test.ts
+++ b/src/earn/utils/buildDepositToMorphoTx.test.ts
@@ -1,8 +1,5 @@
-import {
- MORPHO_VAULT_ABI,
- USDC_ADDRESS,
- USDC_DECIMALS,
-} from '@/earn/constants';
+import { MORPHO_VAULT_ABI } from '@/earn/abis/morpho';
+import { USDC_ADDRESS, USDC_DECIMALS } from '@/earn/constants';
import { encodeFunctionData, parseUnits } from 'viem';
import { describe, expect, it } from 'vitest';
import {
diff --git a/src/earn/utils/buildDepositToMorphoTx.ts b/src/earn/utils/buildDepositToMorphoTx.ts
index 2e4e2578fd..5e7a17ab47 100644
--- a/src/earn/utils/buildDepositToMorphoTx.ts
+++ b/src/earn/utils/buildDepositToMorphoTx.ts
@@ -1,4 +1,4 @@
-import { MORPHO_VAULT_ABI } from '@/earn/constants';
+import { MORPHO_VAULT_ABI } from '@/earn/abis/morpho';
import type { Call } from '@/transaction/types';
import { type Address, encodeFunctionData, erc20Abi } from 'viem';
diff --git a/src/earn/utils/buildWithdrawFromMorphoTx.test.ts b/src/earn/utils/buildWithdrawFromMorphoTx.test.ts
index cea719d1e5..4a745308fd 100644
--- a/src/earn/utils/buildWithdrawFromMorphoTx.test.ts
+++ b/src/earn/utils/buildWithdrawFromMorphoTx.test.ts
@@ -1,4 +1,5 @@
-import { MORPHO_VAULT_ABI, USDC_DECIMALS } from '@/earn/constants';
+import { MORPHO_VAULT_ABI } from '@/earn/abis/morpho';
+import { USDC_DECIMALS } from '@/earn/constants';
import { encodeFunctionData, parseUnits } from 'viem';
import { describe, expect, it } from 'vitest';
import {
diff --git a/src/earn/utils/buildWithdrawFromMorphoTx.ts b/src/earn/utils/buildWithdrawFromMorphoTx.ts
index 82e768c53e..16e792d1bc 100644
--- a/src/earn/utils/buildWithdrawFromMorphoTx.ts
+++ b/src/earn/utils/buildWithdrawFromMorphoTx.ts
@@ -1,4 +1,4 @@
-import { MORPHO_VAULT_ABI } from '@/earn/constants';
+import { MORPHO_VAULT_ABI } from '@/earn/abis/morpho';
import type { Call } from '@/transaction/types';
import { type Address, encodeFunctionData } from 'viem';
@@ -11,11 +11,11 @@ export type WithdrawFromMorphoArgs = {
receiverAddress: Address;
};
-export async function buildWithdrawFromMorphoTx({
+export function buildWithdrawFromMorphoTx({
vaultAddress,
amount,
receiverAddress,
-}: WithdrawFromMorphoArgs): Promise {
+}: WithdrawFromMorphoArgs): Call[] {
const withdrawTxData = encodeFunctionData({
abi: MORPHO_VAULT_ABI,
functionName: 'withdraw',