Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add transaction builder functions for interacting with Morpho vaults #1852

Merged
merged 12 commits into from
Jan 22, 2025
21 changes: 21 additions & 0 deletions src/earn/components/EarnProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { EarnProvider, useEarnContext } from './EarnProvider';

describe('EarnProvider', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<EarnProvider vaultAddress="0x123">{children}</EarnProvider>
);

it('provides vault address through context', () => {
const { result } = renderHook(() => useEarnContext(), { wrapper });

expect(result.current.vaultAddress).toBe('0x123');
});

it('throws error when useEarnContext is used outside of provider', () => {
expect(() => renderHook(() => useEarnContext())).toThrow(
'useEarnContext must be used within an EarnProvider',
);
});
});
30 changes: 30 additions & 0 deletions src/earn/components/EarnProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useValue } from '@/core-react/internal/hooks/useValue';
import { type ReactNode, createContext, useContext } from 'react';
import type { Address } from 'viem';

interface EarnContextType {
vaultAddress: Address;
}

const EarnContext = createContext<EarnContextType | undefined>(undefined);

interface EarnProviderProps {
vaultAddress: Address;
children: ReactNode;
}

export function EarnProvider({ vaultAddress, children }: EarnProviderProps) {
const value = useValue({
vaultAddress,
});

return <EarnContext.Provider value={value}>{children}</EarnContext.Provider>;
}

export function useEarnContext() {
const context = useContext(EarnContext);
if (!context) {
throw new Error('useEarnContext must be used within an EarnProvider');
}
return context;
}
Comment on lines +24 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

our pattern has not been to throw if useComponentContexts have been used outside of their providers. Perhaps that's bad practice, but it's useful in certain cases (eg. if you want to attempt to use a context, and if you get back values use them, and if not source them from elsewhere).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're not throwing these errors in our context hooks, that seems like a code smell. It's the standard way of doing it for pretty much all major libraries.

We also do this in:

  • useNFTLifecycleContext
  • useTheme
  • useBuyContext
  • useCheckoutContext
  • useTransactionContext

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we should include this in all providers

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as I know, wallet is the exception so it will silently fail on an unconnected wallet button

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the providers that do not currently do this are:
OnchainKitProvider
image

IdentityProvider
image

WalletProvider
image

can y'all think of a reason we wouldn't want to throw in these cases? if not I can add

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a big issue that we don't throw in useOnchainKit especially

26 changes: 26 additions & 0 deletions src/earn/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const METAMORPHO_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;
2 changes: 2 additions & 0 deletions src/earn/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// 🌲☀🌲
// Components
89 changes: 89 additions & 0 deletions src/earn/utils/deposit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { METAMORPHO_ABI, USDC_ADDRESS, USDC_DECIMALS } from '@/earn/constants';
import { encodeFunctionData, parseUnits } from 'viem';
import { describe, expect, it } from 'vitest';
import { buildDepositToMorphoTx } from './deposit';

describe('buildDepositToMorphoTx', () => {
const mockArgs = {
vaultAddress: '0xd63070114470f685b75B74D60EEc7c1113d33a3D',
tokenAddress: USDC_ADDRESS,
amount: parseUnits('1000', USDC_DECIMALS),
receiverAddress: '0x9E95f497a7663B70404496dB6481c890C4825fe1',
} as const;

it('should return an array with two transactions', async () => {
const result = buildDepositToMorphoTx(mockArgs);
expect(result).toHaveLength(2);
});

it('should build correct approve transaction', async () => {
const result = buildDepositToMorphoTx(mockArgs);
const expectedAmount = mockArgs.amount;

const expectedApproveData = encodeFunctionData({
abi: [
{
name: 'approve',
type: 'function',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ type: 'bool' }],
stateMutability: 'nonpayable',
},
],
functionName: 'approve',
args: [mockArgs.vaultAddress, expectedAmount],
});

expect(result[0]).toEqual({
to: mockArgs.tokenAddress,
data: expectedApproveData,
});
});

it('should build correct deposit transaction', async () => {
const result = buildDepositToMorphoTx(mockArgs);
const expectedAmount = mockArgs.amount;

const expectedDepositData = encodeFunctionData({
abi: METAMORPHO_ABI,
functionName: 'deposit',
args: [expectedAmount, mockArgs.receiverAddress],
});

expect(result[1]).toEqual({
to: mockArgs.vaultAddress,
data: expectedDepositData,
});
});

it('should handle zero amount', async () => {
const result = buildDepositToMorphoTx({
...mockArgs,
amount: 0n,
});

expect(result).toHaveLength(2);
expect(result[0].to).toBe(mockArgs.tokenAddress);
expect(result[1].to).toBe(mockArgs.vaultAddress);
});

it('should handle decimal amounts', async () => {
const result = buildDepositToMorphoTx({
...mockArgs,
amount: parseUnits('100.5', USDC_DECIMALS),
});

const expectedAmount = parseUnits('100.5', USDC_DECIMALS);
expect(result).toHaveLength(2);
expect(
encodeFunctionData({
abi: METAMORPHO_ABI,
functionName: 'deposit',
args: [expectedAmount, mockArgs.receiverAddress],
}),
).toBe(result[1].data);
});
});
42 changes: 42 additions & 0 deletions src/earn/utils/deposit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { METAMORPHO_ABI } from '@/earn/constants';
import type { Call } from '@/transaction/types';
import { type Address, encodeFunctionData, erc20Abi } from 'viem';

type DepositToMorphoArgs = {
vaultAddress: Address;
tokenAddress: Address;
amount: bigint;
receiverAddress: Address;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add descriptions for each of theses plz?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


export function buildDepositToMorphoTx({
vaultAddress,
tokenAddress,
amount,
receiverAddress,
}: DepositToMorphoArgs): Call[] {
// User needs to approve the token they're depositing
const approveTxData = encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [vaultAddress, amount],
});

// Once approved, user can deposit the token into the vault
const depositTxData = encodeFunctionData({
abi: METAMORPHO_ABI,
functionName: 'deposit',
args: [amount, receiverAddress],
});

return [
{
to: tokenAddress,
data: approveTxData,
},
{
to: vaultAddress,
data: depositTxData,
},
];
}
79 changes: 79 additions & 0 deletions src/earn/utils/withdraw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { METAMORPHO_ABI, USDC_DECIMALS } from '@/earn/constants';
import { encodeFunctionData, parseUnits } from 'viem';
import { describe, expect, it } from 'vitest';
import { buildWithdrawFromMorphoTx } from './withdraw';

describe('buildWithdrawFromMorphoTx', () => {
const mockArgs = {
vaultAddress: '0xd63070114470f685b75B74D60EEc7c1113d33a3D',
amount: parseUnits('1000', USDC_DECIMALS),
receiverAddress: '0x9E95f497a7663B70404496dB6481c890C4825fe1',
} as const;

it('should return an array with one transaction', async () => {
const result = await buildWithdrawFromMorphoTx(mockArgs);
expect(result).toHaveLength(1);
});

it('should build correct withdraw transaction', async () => {
const result = await buildWithdrawFromMorphoTx(mockArgs);
const expectedAmount = mockArgs.amount;

const expectedWithdrawData = encodeFunctionData({
abi: METAMORPHO_ABI,
functionName: 'withdraw',
args: [
expectedAmount,
mockArgs.receiverAddress,
mockArgs.receiverAddress,
],
});

expect(result[0]).toEqual({
to: mockArgs.vaultAddress,
data: expectedWithdrawData,
});
});

it('should handle zero amount', async () => {
const result = await buildWithdrawFromMorphoTx({
...mockArgs,
amount: 0n,
});

const expectedAmount = parseUnits('0', USDC_DECIMALS);
expect(result).toHaveLength(1);
expect(
encodeFunctionData({
abi: METAMORPHO_ABI,
functionName: 'withdraw',
args: [
expectedAmount,
mockArgs.receiverAddress,
mockArgs.receiverAddress,
],
}),
).toBe(result[0].data);
});

it('should handle decimal amounts', async () => {
const result = await buildWithdrawFromMorphoTx({
...mockArgs,
amount: parseUnits('100.5', USDC_DECIMALS),
});

const expectedAmount = parseUnits('100.5', USDC_DECIMALS);
expect(result).toHaveLength(1);
expect(
encodeFunctionData({
abi: METAMORPHO_ABI,
functionName: 'withdraw',
args: [
expectedAmount,
mockArgs.receiverAddress,
mockArgs.receiverAddress,
],
}),
).toBe(result[0].data);
});
});
28 changes: 28 additions & 0 deletions src/earn/utils/withdraw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { METAMORPHO_ABI } from '@/earn/constants';
import type { Call } from '@/transaction/types';
import { type Address, encodeFunctionData } from 'viem';

type WithdrawFromMorphoArgs = {
vaultAddress: Address;
amount: bigint;
receiverAddress: Address;
};

export async function buildWithdrawFromMorphoTx({
vaultAddress,
amount,
receiverAddress,
}: WithdrawFromMorphoArgs): Promise<Call[]> {
const withdrawTxData = encodeFunctionData({
abi: METAMORPHO_ABI,
functionName: 'withdraw',
args: [amount, receiverAddress, receiverAddress],
});

return [
{
to: vaultAddress,
data: withdrawTxData,
},
];
}
Loading