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',