-
Notifications
You must be signed in to change notification settings - Fork 256
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: more send utilities #1892
Merged
+457
−0
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
56fe3de
add helper functions
brendan-defi c05a662
add tests
brendan-defi 53f7a65
fix lints
brendan-defi f73f86f
refactor for comments
brendan-defi 79f148e
fix error message
brendan-defi da57f9f
update error handling
brendan-defi 8d60733
remove duplicate stringification
brendan-defi 4f740a9
add tests
brendan-defi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { getAddress } from '@/identity/utils/getAddress'; | ||
import { isBasename } from '@/identity/utils/isBasename'; | ||
import { isEns } from '@/identity/utils/isEns'; | ||
import { isAddress } from 'viem'; | ||
import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
import { validateAddressInput } from './validateAddressInput'; | ||
|
||
vi.mock('@/identity/utils/isBasename', () => ({ | ||
isBasename: vi.fn(), | ||
})); | ||
|
||
vi.mock('@/identity/utils/isEns', () => ({ | ||
isEns: vi.fn(), | ||
})); | ||
|
||
vi.mock('@/identity/utils/getAddress', () => ({ | ||
getAddress: vi.fn(), | ||
})); | ||
|
||
vi.mock('viem', async () => ({ | ||
isAddress: vi.fn(), | ||
})); | ||
|
||
describe('validateAddressInput', () => { | ||
beforeEach(() => { | ||
vi.clearAllMocks(); | ||
}); | ||
|
||
it('validates basename and returns resolved address on base', async () => { | ||
const mockAddress = '0x1234567890123456789012345678901234567890'; | ||
vi.mocked(isBasename).mockReturnValue(true); | ||
vi.mocked(isEns).mockReturnValue(false); | ||
vi.mocked(getAddress).mockResolvedValue(mockAddress); | ||
|
||
const result = await validateAddressInput('test.base'); | ||
|
||
expect(getAddress).toHaveBeenCalledWith({ | ||
name: 'test.base', | ||
chain: expect.objectContaining({ id: 8453 }), | ||
}); | ||
expect(result).toBe(mockAddress); | ||
}); | ||
|
||
it('validates ENS and returns resolved address on mainnet', async () => { | ||
const mockAddress = '0x1234567890123456789012345678901234567890'; | ||
vi.mocked(isBasename).mockReturnValue(false); | ||
vi.mocked(isEns).mockReturnValue(true); | ||
vi.mocked(getAddress).mockResolvedValue(mockAddress); | ||
|
||
const result = await validateAddressInput('test.eth'); | ||
|
||
expect(getAddress).toHaveBeenCalledWith({ | ||
name: 'test.eth', | ||
chain: expect.objectContaining({ id: 1 }), | ||
}); | ||
expect(result).toBe(mockAddress); | ||
}); | ||
|
||
it('returns null when name resolution fails', async () => { | ||
vi.mocked(isBasename).mockReturnValue(true); | ||
vi.mocked(isEns).mockReturnValue(false); | ||
vi.mocked(getAddress).mockResolvedValue(null); | ||
const basenameResult = await validateAddressInput('invalid.base'); | ||
expect(basenameResult).toBeNull(); | ||
|
||
vi.mocked(isBasename).mockReturnValue(false); | ||
vi.mocked(isEns).mockReturnValue(true); | ||
vi.mocked(getAddress).mockResolvedValue(null); | ||
const ensResult = await validateAddressInput('invalid.eth'); | ||
expect(ensResult).toBeNull(); | ||
}); | ||
|
||
it('validates and returns raw ethereum address', async () => { | ||
const mockAddress = '0x1234567890123456789012345678901234567890'; | ||
vi.mocked(isBasename).mockReturnValue(false); | ||
vi.mocked(isEns).mockReturnValue(false); | ||
vi.mocked(isAddress).mockReturnValue(true); | ||
|
||
const result = await validateAddressInput(mockAddress); | ||
|
||
expect(isAddress).toHaveBeenCalledWith(mockAddress, { strict: false }); | ||
expect(result).toBe(mockAddress); | ||
}); | ||
|
||
it('returns null for invalid input', async () => { | ||
vi.mocked(isBasename).mockReturnValue(false); | ||
vi.mocked(isEns).mockReturnValue(false); | ||
vi.mocked(isAddress).mockReturnValue(false); | ||
|
||
const result = await validateAddressInput('invalid input'); | ||
|
||
expect(result).toBeNull(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { getAddress } from '@/identity/utils/getAddress'; | ||
import { isBasename } from '@/identity/utils/isBasename'; | ||
import { isEns } from '@/identity/utils/isEns'; | ||
import { isAddress } from 'viem'; | ||
import { base, mainnet } from 'viem/chains'; | ||
|
||
export async function validateAddressInput(input: string) { | ||
if (isAddress(input, { strict: false })) { | ||
return input; | ||
} | ||
|
||
if (isBasename(input) || isEns(input)) { | ||
const address = await getAddress({ | ||
name: input, | ||
chain: isBasename(input) ? base : mainnet, | ||
}); | ||
if (address) { | ||
return address; | ||
} | ||
} | ||
|
||
return null; | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: if isAddress then no need to check if its isBasename or isEns? moving this first could remove the else if and short circuit faster
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense, updated