Skip to content

Commit

Permalink
feat: more send utilities (#1892)
Browse files Browse the repository at this point in the history
  • Loading branch information
brendan-defi authored Jan 28, 2025
1 parent 37d695d commit 8607454
Show file tree
Hide file tree
Showing 7 changed files with 457 additions and 0 deletions.
84 changes: 84 additions & 0 deletions src/api/buildSendTransaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { type Address, encodeFunctionData, erc20Abi } from 'viem';
import { type Mock, describe, expect, it, vi } from 'vitest';
import { buildSendTransaction } from './buildSendTransaction';

vi.mock('viem', async () => {
const actual = await vi.importActual('viem');
return {
...actual,
encodeFunctionData: vi.fn(),
};
});

describe('buildSendTransaction', () => {
const mockRecipient = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e';
const mockToken = '0x1234567890123456789012345678901234567890';
const mockAmount = 1000000000000000000n; // 1 ETH/token in wei

it('should build native ETH transfer transaction', () => {
const result = buildSendTransaction({
recipientAddress: mockRecipient,
tokenAddress: undefined as unknown as Address, // type assertion okay because we're testing the case where tokenAddress is undefined
amount: mockAmount,
});

expect(result).toEqual({
to: mockRecipient,
data: '0x',
value: mockAmount,
});
});

it('should build ERC20 token transfer transaction', () => {
const expectedCallData = encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [mockRecipient, mockAmount],
});

const result = buildSendTransaction({
recipientAddress: mockRecipient,
tokenAddress: mockToken,
amount: mockAmount,
});

expect(result).toEqual({
to: mockToken,
data: expectedCallData,
});
});

it('should handle Error objects', () => {
(encodeFunctionData as Mock).mockImplementation(() => {
throw new Error('Test error');
});
const result = buildSendTransaction({
recipientAddress: mockRecipient,
tokenAddress: mockToken,
amount: mockAmount,
});

expect(result).toMatchObject({
code: 'AmBSeTa01',
message: 'Could not build transfer transaction',
error: 'Test error',
});
});

it('should handle non-Error objects', () => {
(encodeFunctionData as Mock).mockImplementation(() => {
throw 'Some string error';
});
const result = buildSendTransaction({
recipientAddress: mockRecipient,
tokenAddress: mockToken,
amount: mockAmount,
});

expect(result).toMatchObject({
code: 'AmBSeTa01',
message: 'Could not build transfer transaction',
error: 'Some string error',
});
});
});
40 changes: 40 additions & 0 deletions src/api/buildSendTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { encodeFunctionData, erc20Abi } from 'viem';
import type {
BuildSendTransactionParams,
BuildSendTransactionResponse,
} from './types';

export function buildSendTransaction({
recipientAddress,
tokenAddress,
amount,
}: BuildSendTransactionParams): BuildSendTransactionResponse {
// if no token address, we are sending native ETH
// and the data prop is empty
if (!tokenAddress) {
return {
to: recipientAddress,
data: '0x',
value: amount,
};
}

try {
const transferCallData = encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipientAddress, amount],
});
return {
to: tokenAddress,
data: transferCallData,
};
} catch (error) {
const message = error instanceof Error ? error.message : `${error}`;
return {
code: 'AmBSeTa01', // Api Module Build Send Transaction Error 01
error: message,
message: 'Could not build transfer transaction',
};
}
}
18 changes: 18 additions & 0 deletions src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Address } from 'viem';
import type { Fee, QuoteWarning, SwapQuote, Transaction } from '../swap/types';
import type { Token } from '../token/types';
import type { Call } from '../transaction/types';

export type AddressOrETH = Address | 'ETH';

Expand Down Expand Up @@ -366,3 +367,20 @@ export type GetPortfoliosResponse = {
/** The portfolios for the provided addresses */
portfolios: Portfolio[];
};

/**
* Note: exported as public Type
*/
export type BuildSendTransactionParams = {
/** The address of the recipient */
recipientAddress: Address;
/** The address of the token to transfer */
tokenAddress: Address | null;
/** The amount of the transfer */
amount: bigint;
};

/**
* Note: exported as public Type
*/
export type BuildSendTransactionResponse = Call | APIError;
148 changes: 148 additions & 0 deletions src/internal/hooks/useSendTransaction.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { buildSendTransaction } from '@/api/buildSendTransaction';
import type { Token } from '@/token';
import { renderHook } from '@testing-library/react';
import { type Address, parseUnits } from 'viem';
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';
import { useSendTransaction } from './useSendTransaction';

vi.mock('@/api/buildSendTransaction', () => ({
buildSendTransaction: vi.fn(),
}));

describe('useSendTransaction', () => {
const mockToken: Token = {
symbol: 'TOKEN',
decimals: 18,
address: '0x0987654321098765432109876543210987654321',
chainId: 8453,
image: '',
name: '',
};
const mockRecipientAddress = '0x1234567890123456789012345678901234567890';
const mockCallData = {
to: mockRecipientAddress,
data: mockToken.address,
value: parseUnits('1.0', 18),
};

beforeEach(() => {
vi.resetAllMocks();
});

it('returns empty calls when token is null', () => {
const { result } = renderHook(() =>
useSendTransaction({
recipientAddress: mockRecipientAddress,
token: null,
amount: '1.0',
}),
);

expect(result.current).toEqual({
code: 'AmBSeTx01', // Api Module Build Send Transaction Error 01
error: 'No token provided',
message: 'Could not build send transaction',
});
});

it('handles ETH transfers correctly', () => {
(buildSendTransaction as Mock).mockReturnValue({
...mockCallData,
data: '',
});
const { result } = renderHook(() =>
useSendTransaction({
recipientAddress: mockRecipientAddress,
token: { ...mockToken, address: '', symbol: 'ETH' },
amount: '1.0',
}),
);

expect(buildSendTransaction).toHaveBeenCalledWith({
recipientAddress: mockRecipientAddress,
tokenAddress: null,
amount: parseUnits('1.0', 18),
});
expect(result.current).toEqual({
to: mockRecipientAddress,
data: '',
value: parseUnits('1.0', 18),
});
});

it('returns error for non-ETH token without address', () => {
const { result } = renderHook(() =>
useSendTransaction({
recipientAddress: mockRecipientAddress,
token: {
...mockToken,
symbol: 'INVALID',
address: undefined as unknown as Address, // type assertion okay because we're testing the case where address is undefined
},
amount: '1.0',
}),
);

expect(result.current).toEqual({
code: 'AmBSeTx02', // Api Module Build Send Transaction Error 02
error: 'No token address provided for non-ETH token',
message: 'Could not build send transaction',
});
});

it('handles ERC20 token transfers correctly', () => {
const mockDecimals = 6;
const expectedCallData = {
to: mockRecipientAddress,
data: mockToken.address,
value: parseUnits('100', mockDecimals),
};
(buildSendTransaction as Mock).mockReturnValue(expectedCallData);

renderHook(() =>
useSendTransaction({
recipientAddress: mockRecipientAddress,
token: {
...mockToken,
symbol: 'USDC',
address: mockToken.address,
decimals: 6,
},
amount: '100',
}),
);
expect(buildSendTransaction).toHaveBeenCalledWith({
recipientAddress: mockRecipientAddress,
tokenAddress: mockToken.address,
amount: parseUnits('100', 6),
});
});

it('handles different decimal places correctly', () => {
const mockDecimals = 12;
const expectedCallData = {
to: mockRecipientAddress,
data: mockToken.address,
value: parseUnits('0.5', mockDecimals),
};
(buildSendTransaction as Mock).mockReturnValue(expectedCallData);

renderHook(() =>
useSendTransaction({
recipientAddress: mockRecipientAddress,
token: {
...mockToken,
symbol: 'TEST',
address: mockToken.address,
decimals: mockDecimals,
},
amount: '0.5',
}),
);
expect(buildSendTransaction).toHaveBeenCalledWith({
recipientAddress: mockRecipientAddress,
tokenAddress: mockToken.address,
amount: parseUnits('0.5', mockDecimals),
});
});
});
50 changes: 50 additions & 0 deletions src/internal/hooks/useSendTransaction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { buildSendTransaction } from '@/api/buildSendTransaction';
import type { BuildSendTransactionResponse } from '@/api/types';
import type { Token } from '@/token';
import { type Address, parseUnits } from 'viem';

type UseSendTransactionParams = {
recipientAddress: Address;
token: Token | null;
amount: string;
};

export function useSendTransaction({
recipientAddress,
token,
amount,
}: UseSendTransactionParams): BuildSendTransactionResponse {
if (!token) {
return {
code: 'AmBSeTx01', // Api Module Build Send Transaction Error 01
error: 'No token provided',
message: 'Could not build send transaction',
};
}

if (!token.address) {
if (token.symbol !== 'ETH') {
return {
code: 'AmBSeTx02', // Api Module Build Send Transaction Error 02
error: 'No token address provided for non-ETH token',
message: 'Could not build send transaction',
};
}
const parsedAmount = parseUnits(amount, token.decimals);
const sendTransaction = buildSendTransaction({
recipientAddress,
tokenAddress: null,
amount: parsedAmount,
});
return sendTransaction;
}

const parsedAmount = parseUnits(amount, token.decimals);
const sendTransaction = buildSendTransaction({
recipientAddress,
tokenAddress: token.address,
amount: parsedAmount,
});

return sendTransaction;
}
Loading

0 comments on commit 8607454

Please sign in to comment.