Skip to content

Commit

Permalink
fix: improve Earn transaction success state (#1973)
Browse files Browse the repository at this point in the history
  • Loading branch information
dschlabach authored Feb 13, 2025
1 parent 08e073c commit 2572548
Show file tree
Hide file tree
Showing 25 changed files with 228 additions and 27 deletions.
1 change: 1 addition & 0 deletions playground/nextjs-app-router/lib/url-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const URL_PARAM_MAPPING: Partial<Record<OnchainKitComponent, string[]>> = {
[OnchainKitComponent.NFTCardDefault]: ['chainId', 'nftToken'],
[OnchainKitComponent.NFTMintCard]: ['chainId', 'nftToken'],
[OnchainKitComponent.NFTMintCardDefault]: ['chainId', 'nftToken'],
[OnchainKitComponent.Earn]: ['vaultAddress'],
};

export function getShareableUrl(activeComponent?: OnchainKitComponent) {
Expand Down
6 changes: 3 additions & 3 deletions playground/nextjs-app-router/onchainkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinbase/onchainkit",
"version": "0.36.10",
"version": "0.36.11",
"type": "module",
"repository": "https://github.com/coinbase/onchainkit.git",
"license": "MIT",
Expand Down Expand Up @@ -42,8 +42,8 @@
"qrcode": "^1.5.4",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"viem": "^2.21.33",
"wagmi": "^2.12.24"
"viem": "^2.23.0",
"wagmi": "^2.14.11"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
Expand Down
1 change: 0 additions & 1 deletion src/appchain/bridge/utils/maybeAddProofNode.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { type Hex, fromRlp, toRlp } from 'viem';

/* v8 ignore start */
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: ignore
export function maybeAddProofNode(key: string, proof: readonly Hex[]) {
const lastProofRlp = proof[proof.length - 1];
const lastProof = fromRlp(lastProofRlp);
Expand Down
15 changes: 13 additions & 2 deletions src/earn/components/DepositButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import {
type TransactionResponse,
} from '@/transaction';
import { ConnectWallet } from '@/wallet';
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import type { DepositButtonReact } from '../types';
import { useEarnContext } from './EarnProvider';

export function DepositButton({ className }: DepositButtonReact) {
const {
recipientAddress: address,
vaultToken,
depositCalls,
depositAmount,
setDepositAmount,
Expand All @@ -21,6 +22,8 @@ export function DepositButton({ className }: DepositButtonReact) {
refetchWalletBalance,
} = useEarnContext();

const [depositedAmount, setDepositedAmount] = useState('');

const handleOnStatus = useCallback(
(status: LifecycleStatus) => {
if (status.statusName === 'transactionPending') {
Expand All @@ -44,11 +47,15 @@ export function DepositButton({ className }: DepositButtonReact) {
res.transactionReceipts[0] &&
res.transactionReceipts[0].status === 'success'
) {
// Don't overwrite to '' when the second txn comes in
if (depositAmount) {
setDepositedAmount(depositAmount);
}
setDepositAmount('');
refetchWalletBalance();
}
},
[setDepositAmount, refetchWalletBalance],
[depositAmount, setDepositAmount, refetchWalletBalance],
);

if (!address) {
Expand All @@ -66,9 +73,13 @@ export function DepositButton({ className }: DepositButtonReact) {
calls={depositCalls}
onStatus={handleOnStatus}
onSuccess={handleOnSuccess}
resetAfter={3_000}
>
<TransactionButton
text={depositAmountError ?? 'Deposit'}
successOverride={{
text: `Deposited ${depositedAmount} ${vaultToken?.symbol}`,
}}
disabled={!!depositAmountError || !depositAmount}
/>
</Transaction>
Expand Down
2 changes: 1 addition & 1 deletion src/earn/components/EarnDeposit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function EarnDepositDefaultContent() {
<EarnDetails />
<DepositAmountInput />
<DepositBalance />
<DepositButton className="h-12" />
<DepositButton className="-mt-4 h-12" />
</>
);
}
Expand Down
12 changes: 12 additions & 0 deletions src/earn/components/EarnDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ describe('EarnDetails Component', () => {
mockUseEarnContext.mockReturnValue(MOCK_EARN_CONTEXT);
});

it('renders error message when error is present', () => {
mockUseEarnContext.mockReturnValue({
...MOCK_EARN_CONTEXT,
error: new Error('Test error'),
});

render(<EarnDetails />);

const errorMessage = screen.getByText('Error fetching vault details');
expect(errorMessage).toBeInTheDocument();
});

it('applies custom className when provided', () => {
const customClass = 'custom-class';
render(<EarnDetails className={customClass} />);
Expand Down
14 changes: 13 additions & 1 deletion src/earn/components/EarnDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { useEarnContext } from '@/earn/components/EarnProvider';
import { VaultDetails } from '@/earn/components/VaultDetails';
import { YieldDetails } from '@/earn/components/YieldDetails';
import { border, cn } from '@/styles/theme';
import { border, cn, color } from '@/styles/theme';
import type { EarnDetailsReact } from '../types';

export function EarnDetails({ className }: EarnDetailsReact) {
const { error } = useEarnContext();

if (error) {
return (
<div className={cn('flex w-full flex-col gap-1 text-sm', color.error)}>
<div className="font-semibold">Error fetching vault details</div>
<div className="text-xs">{error.message}</div>
</div>
);
}

return (
<div
data-testid="ockEarnDetails"
Expand Down
2 changes: 2 additions & 0 deletions src/earn/components/EarnProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function EarnProvider({ vaultAddress, children }: EarnProviderReact) {
deposits,
liquidity,
rewards,
error,
} = useMorphoVault({
vaultAddress,
recipientAddress: address,
Expand Down Expand Up @@ -137,6 +138,7 @@ export function EarnProvider({ vaultAddress, children }: EarnProviderReact) {
}, [withdrawAmount, depositedBalance]);

const value = useValue<EarnContextType>({
error,
recipientAddress: address,
vaultAddress,
vaultToken,
Expand Down
2 changes: 1 addition & 1 deletion src/earn/components/EarnWithdraw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function EarnWithdrawDefaultContent() {
<EarnDetails />
<WithdrawAmountInput />
<WithdrawBalance />
<WithdrawButton className="h-12" />
<WithdrawButton className="-mt-4 h-12" />
</>
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/earn/components/VaultDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ export function VaultDetails() {
data-testid="ock-vaultDetails"
>
<TokenImage token={vaultToken} size={16} />
<span className="max-w-24 truncate">{vaultName}</span>
<span className="max-w-24 truncate" title={vaultName}>
{vaultName}
</span>
<button
ref={triggerRef}
type="button"
Expand Down Expand Up @@ -70,7 +72,7 @@ export function VaultDetails() {
color.foreground,
border.defaultActive,
background.default,
'flex min-w-40 flex-col gap-3 rounded-lg border p-2 text-sm',
'flex min-w-40 flex-col gap-3 rounded-lg border p-3 text-sm',
'fade-in animate-in duration-200',
)}
>
Expand Down
10 changes: 6 additions & 4 deletions src/earn/components/WithdrawButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ vi.mock('@/transaction', async (importOriginal) => {
>
Success
</button>

<button
type="button"
data-testid="transaction-button"
Expand Down Expand Up @@ -129,10 +130,10 @@ describe('WithdrawButton Component', () => {
});

it('renders Transaction with depositCalls from EarnProvider', () => {
const mockDepositCalls = [{ to: '0x123', data: '0x456' }] as Call[];
const mockWithdrawCalls = [{ to: '0x123', data: '0x456' }] as Call[];
vi.mocked(useEarnContext).mockReturnValue({
...baseContext,
depositCalls: mockDepositCalls,
withdrawCalls: mockWithdrawCalls,
});

render(<WithdrawButton />);
Expand All @@ -144,7 +145,8 @@ describe('WithdrawButton Component', () => {
it('renders TransactionButton with the correct text', () => {
vi.mocked(useEarnContext).mockReturnValue({
...baseContext,
depositCalls: [],
withdrawAmount: '',
withdrawCalls: [],
});

const { container } = render(<WithdrawButton />);
Expand All @@ -156,7 +158,7 @@ describe('WithdrawButton Component', () => {
const mockUpdateLifecycleStatus = vi.fn();
vi.mocked(useEarnContext).mockReturnValue({
...baseContext,
depositCalls: [{ to: '0x123', data: '0x456' }],
withdrawCalls: [{ to: '0x123', data: '0x456' }],
updateLifecycleStatus: mockUpdateLifecycleStatus,
});

Expand Down
15 changes: 13 additions & 2 deletions src/earn/components/WithdrawButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {
type TransactionResponse,
} from '@/transaction';
import { ConnectWallet } from '@/wallet';
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import type { WithdrawButtonReact } from '../types';
import { useEarnContext } from './EarnProvider';

export function WithdrawButton({ className }: WithdrawButtonReact) {
const {
recipientAddress: address,
Expand All @@ -18,8 +19,11 @@ export function WithdrawButton({ className }: WithdrawButtonReact) {
updateLifecycleStatus,
refetchDepositedBalance,
withdrawAmountError,
vaultToken,
} = useEarnContext();

const [withdrawnAmount, setWithdrawnAmount] = useState('');

const handleOnStatus = useCallback(
(status: LifecycleStatus) => {
if (status.statusName === 'transactionPending') {
Expand All @@ -43,11 +47,14 @@ export function WithdrawButton({ className }: WithdrawButtonReact) {
res.transactionReceipts[0] &&
res.transactionReceipts[0].status === 'success'
) {
if (withdrawAmount) {
setWithdrawnAmount(withdrawAmount);
}
setWithdrawAmount('');
refetchDepositedBalance();
}
},
[setWithdrawAmount, refetchDepositedBalance],
[setWithdrawAmount, refetchDepositedBalance, withdrawAmount],
);

if (!address) {
Expand All @@ -65,9 +72,13 @@ export function WithdrawButton({ className }: WithdrawButtonReact) {
calls={withdrawCalls}
onStatus={handleOnStatus}
onSuccess={handleOnSuccess}
resetAfter={3_000}
>
<TransactionButton
text={withdrawAmountError ?? 'Withdraw'}
successOverride={{
text: `Withdrew ${withdrawnAmount} ${vaultToken?.symbol}`,
}}
disabled={!!withdrawAmountError || !withdrawAmount}
/>
</Transaction>
Expand Down
1 change: 1 addition & 0 deletions src/earn/hooks/useMorphoVault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe('useMorphoVault', () => {

expect(result.current).toEqual({
status: 'pending',
error: null,
asset: {
address: undefined,
symbol: undefined,
Expand Down
5 changes: 4 additions & 1 deletion src/earn/hooks/useMorphoVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type UseMorphoVaultParams = {
export type UseMorphoVaultReturnType = {
vaultName: string | undefined;
status: 'pending' | 'success' | 'error';
/** Warns users if vault address is invalid */
error: Error | null;
/** Underlying asset of the vault */
asset: {
address: Address;
Expand Down Expand Up @@ -97,7 +99,7 @@ export function useMorphoVault({
},
});

const { data: vaultData } = useQuery({
const { data: vaultData, error } = useQuery({
queryKey: ['morpho-apy', vaultAddress],
queryFn: () => fetchMorphoApy(vaultAddress),
});
Expand Down Expand Up @@ -129,6 +131,7 @@ export function useMorphoVault({

return {
status,
error,
/** Balance is the amount of the underlying asset that the user has in the vault */
balance: formattedBalance,
balanceStatus,
Expand Down
1 change: 1 addition & 0 deletions src/earn/mocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Address } from 'viem';
import { vi } from 'vitest';

export const MOCK_EARN_CONTEXT: EarnContextType = {
error: null,
walletBalance: '1000',
walletBalanceStatus: 'success',
refetchWalletBalance: vi.fn(),
Expand Down
2 changes: 2 additions & 0 deletions src/earn/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export type EarnProviderReact = {
* Note: exported as public Type
*/
export type EarnContextType = {
/** Warns users if vault address is invalid */
error: Error | null;
recipientAddress?: Address;
/** Balance of the underlying asset in the user's wallet, converted to the asset's decimals */
walletBalance?: string;
Expand Down
34 changes: 33 additions & 1 deletion src/earn/utils/fetchMorphoApy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const mockVaultResponse: MorphoVaultApiResponse = {
},
},
},
errors: null,
};

describe('fetchMorphoApy', () => {
Expand Down Expand Up @@ -77,7 +78,7 @@ describe('fetchMorphoApy', () => {
expect(result).toEqual(mockVaultResponse.data.vaultByAddress);
});

it('handles API errors', async () => {
it('handles fetch errors', async () => {
(global.fetch as Mock).mockRejectedValueOnce(new Error('API Error'));

const vaultAddress = '0x1234567890123456789012345678901234567890';
Expand All @@ -92,4 +93,35 @@ describe('fetchMorphoApy', () => {
const vaultAddress = '0x1234567890123456789012345678901234567890';
await expect(fetchMorphoApy(vaultAddress)).rejects.toThrow();
});

it('handles bad user input', async () => {
(global.fetch as Mock).mockResolvedValueOnce({
json: () =>
Promise.resolve({
errors: [{ message: 'Bad User Input', status: 'BAD_USER_INPUT' }],
}),
});

const vaultAddress = '0x1234567890123456789012345678901234567890';
await expect(fetchMorphoApy(vaultAddress)).rejects.toThrow(
'Vault not found. Ensure the address is a valid Morpho vault on Base.',
);
});

it('handles generic errors', async () => {
(global.fetch as Mock).mockResolvedValueOnce({
json: () =>
Promise.resolve({
errors: [
{ message: 'Generic Error', status: 'INTERNAL_SERVER_ERROR' },
],
}),
});

expect(
fetchMorphoApy('0x1234567890123456789012345678901234567890'),
).rejects.toThrow(
'Error fetching Morpho vault data. Please try again later.',
);
});
});
Loading

0 comments on commit 2572548

Please sign in to comment.