Skip to content

Commit

Permalink
chore(eth-fees): calculate eth send fees on SendEnterAmount (#4491)
Browse files Browse the repository at this point in the history
### Description

uses our prepareTransactions helper to get the cost of sending eth, and
displays it on SendEnterAmount screen

### Test plan

- jest
- manual test on dev build


https://github.com/valora-inc/wallet/assets/7979215/cf90ab42-2256-4922-a582-9041d9d44cdd


### Related issues

fixes https://linear.app/valora/issue/ACT-956/ethereum-send-fees

### Backwards compatibility

na
  • Loading branch information
cajubelt authored Nov 20, 2023
1 parent dbbe82b commit 8448cca
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 28 deletions.
29 changes: 27 additions & 2 deletions src/send/usePrepareSendTransactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
import {
PreparedTransactionsResult,
prepareERC20TransferTransaction,
prepareSendNativeAssetTransaction,
prepareTransferWithCommentTransaction,
} from 'src/viem/prepareTransactions'
import { mockCeloTokenBalance } from 'test/values'
import { mockCeloTokenBalance, mockEthTokenBalance } from 'test/values'
import BigNumber from 'bignumber.js'
import mocked = jest.mocked
import { renderHook } from '@testing-library/react-native'
Expand Down Expand Up @@ -69,7 +70,7 @@ describe('usePrepareSendTransactions', () => {
})
).toBeUndefined()
})
it('uses prepareERC20TransferTransaction if token does not support comments', async () => {
it('uses prepareERC20TransferTransaction if token is erc20 and does not support comments', async () => {
mocked(tokenSupportsComments).mockReturnValue(false)
const mockPrepareTransactionsResult: PreparedTransactionsResult = {
type: 'not-enough-balance-for-gas',
Expand Down Expand Up @@ -119,6 +120,30 @@ describe('usePrepareSendTransactions', () => {
comment: 'mock comment',
})
})
it('uses prepareSendNativeAssetTransaction if token is native and does not have address', async () => {
mocked(tokenSupportsComments).mockReturnValue(false)
const mockPrepareTransactionsResult: PreparedTransactionsResult = {
type: 'not-enough-balance-for-gas',
feeCurrencies: [mockEthTokenBalance],
}
mocked(prepareSendNativeAssetTransaction).mockResolvedValue(mockPrepareTransactionsResult)
expect(
await _prepareSendTransactionsCallback({
amount: new BigNumber(0.05),
token: mockEthTokenBalance,
recipientAddress: '0xabc',
walletAddress: '0x123',
feeCurrencies: [mockEthTokenBalance],
})
).toStrictEqual(mockPrepareTransactionsResult)
expect(prepareSendNativeAssetTransaction).toHaveBeenCalledWith({
fromWalletAddress: '0x123',
toWalletAddress: '0xabc',
sendToken: mockEthTokenBalance,
amount: BigInt('5'.concat('0'.repeat(16))),
feeCurrencies: [mockEthTokenBalance],
})
})
})
describe('usePrepareSendTransactions', () => {
// integration tests (testing both usePrepareSendTransactions and _prepareSendTransactionsCallback at once)
Expand Down
32 changes: 22 additions & 10 deletions src/send/usePrepareSendTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { useState } from 'react'
import {
PreparedTransactionsResult,
prepareERC20TransferTransaction,
prepareSendNativeAssetTransaction,
prepareTransferWithCommentTransaction,
} from 'src/viem/prepareTransactions'
import { TokenBalance, tokenBalanceHasAddress } from 'src/tokens/slice'
import { TokenBalance, tokenBalanceHasAddress, isNativeTokenBalance } from 'src/tokens/slice'
import BigNumber from 'bignumber.js'
import { useAsyncCallback } from 'react-async-hook'
import { tokenSupportsComments } from 'src/tokens/utils'
Expand Down Expand Up @@ -32,22 +33,33 @@ export async function _prepareSendTransactionsCallback({
if (amount.isLessThanOrEqualTo(0)) {
return
}
const baseTransactionParams = {
// not including sendToken yet because of typing. need to check whether token has address field or not first, required for erc-20 transfers
fromWalletAddress: walletAddress,
toWalletAddress: recipientAddress,
amount: BigInt(tokenAmountInSmallestUnit(amount, token.decimals)),
feeCurrencies,
}
if (tokenBalanceHasAddress(token)) {
const transactionParams = {
fromWalletAddress: walletAddress,
toWalletAddress: recipientAddress,
sendToken: token,
amount: BigInt(tokenAmountInSmallestUnit(amount, token.decimals)),
feeCurrencies,
comment,
}
// NOTE: CELO will be sent as ERC-20. This makes analytics easier, but if gas prices increase later on and/or we
// gain analytics coverage for native CELO transfers, we could switch to sending CELO as native asset to save on gas
const transactionParams = { ...baseTransactionParams, sendToken: token, comment }
if (tokenSupportsComments(token)) {
return prepareTransferWithCommentTransaction(transactionParams)
} else {
return prepareERC20TransferTransaction(transactionParams)
}
} else if (isNativeTokenBalance(token)) {
return prepareSendNativeAssetTransaction({
...baseTransactionParams,
sendToken: token,
})
} else {
Logger.error(
TAG,
`Token does not have address AND is not native. token: ${JSON.stringify(token)}}`
)
}
// TODO(ACT-956): non-ERC20 native asset case
}

/**
Expand Down
22 changes: 11 additions & 11 deletions src/tokens/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,17 @@ export interface TokenBalance extends BaseToken {
}

// The "WithAddress" suffixed types are legacy types, for places in the wallet
// that require an address to be present. As we move to multichain, (where address
// is not guaranteed,) existing code should be updated to use the "address optional" types.
// that require an address to be present. Many are deprecated because in most places, we should
// be able to handle tokens without addresses the same way (just use tokenId instead if you need a token identifier).
// Exceptions include anything that directly interacts with the blockchain, where it makes a difference
// if a token doesn't have an address.

export interface TokenBalanceWithAddress extends TokenBalance {
address: string
}

// The "WithAddress" suffixed types are legacy types, for places in the wallet
// that require an address to be present. As we move to multichain, (where address
// is not guaranteed,) existing code should be updated to use the "address optional" types.

/**
* @deprecated use `TokenBalance` for new code
*/
export interface TokenBalanceWithAddress extends TokenBalance {
address: string
export interface NativeTokenBalance extends TokenBalance {
isNative: true
}

export interface StoredTokenBalances {
Expand Down Expand Up @@ -114,6 +110,10 @@ export function tokenBalanceHasAddress(
return !!tokenInfo.address
}

export function isNativeTokenBalance(tokenInfo: TokenBalance): tokenInfo is NativeTokenBalance {
return !!tokenInfo.isNative
}

export const initialState = {
tokenBalances: {},
loading: false,
Expand Down
24 changes: 23 additions & 1 deletion src/viem/prepareTransactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import {
prepareTransferWithCommentTransaction,
tryEstimateTransaction,
tryEstimateTransactions,
prepareSendNativeAssetTransaction,
} from 'src/viem/prepareTransactions'
import { mockCeloTokenBalance } from 'test/values'
import { mockCeloTokenBalance, mockEthTokenBalance } from 'test/values'
import {
Address,
BaseError,
Expand Down Expand Up @@ -641,6 +642,27 @@ describe('prepareTransactions module', () => {
})
})

it('prepareSendNativeAssetTransaction', async () => {
const mockPrepareTransactions = jest.fn()
await prepareSendNativeAssetTransaction(
{
fromWalletAddress: '0x123',
toWalletAddress: '0x456',
amount: BigInt(100),
feeCurrencies: [mockEthTokenBalance],
sendToken: mockEthTokenBalance,
},
mockPrepareTransactions
)
expect(mockPrepareTransactions).toHaveBeenCalledWith({
feeCurrencies: [mockEthTokenBalance],
spendToken: mockEthTokenBalance,
spendTokenAmount: new BigNumber(100),
decreasedAmountGasFeeMultiplier: 1,
baseTransactions: [{ from: '0x123', to: '0x456', value: BigInt(100) }],
})
})

it('prepareTransferWithCommentTransaction', async () => {
const mockPrepareTransactions = jest.fn()
mocked(encodeFunctionData).mockReturnValue('0xabc')
Expand Down
45 changes: 42 additions & 3 deletions src/viem/prepareTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TransactionRequestCIP42 } from 'node_modules/viem/_types/chains/celo/ty
import erc20 from 'src/abis/IERC20'
import stableToken from 'src/abis/StableToken'
import { STATIC_GAS_PADDING } from 'src/config'
import { TokenBalance, TokenBalanceWithAddress } from 'src/tokens/slice'
import { NativeTokenBalance, TokenBalance, TokenBalanceWithAddress } from 'src/tokens/slice'
import Logger from 'src/utils/Logger'
import { estimateFeesPerGas } from 'src/viem/estimateFeesPerGas'
import { publicClient } from 'src/viem/index'
Expand Down Expand Up @@ -205,7 +205,7 @@ export async function prepareTransactions({
throwOnSpendTokenAmountExceedsBalance = true,
}: {
feeCurrencies: TokenBalance[]
spendToken: TokenBalanceWithAddress
spendToken: TokenBalance
spendTokenAmount: BigNumber
decreasedAmountGasFeeMultiplier: number
baseTransactions: (TransactionRequest & { gas?: bigint })[]
Expand Down Expand Up @@ -372,7 +372,46 @@ export async function prepareTransferWithCommentTransaction(
})
}

// TODO(ACT-956) create helper for native transfers
/**
* Prepare a transaction for sending native asset.
*
* @param fromWalletAddress - sender address
* @param toWalletAddress - recipient address
* @param amount the amount of the token to send, denominated in the smallest units for that token
* @param feeCurrencies - tokens to consider using for paying the transaction fee
* @param sendToken - native asset to send. MUST be native asset (e.g. sendable using the 'value' field of a transaction, like ETH or CELO)
*
* @param prepareTxs a function that prepares the transactions (for unit testing-- should use default everywhere else)
**/
export function prepareSendNativeAssetTransaction(
{
fromWalletAddress,
toWalletAddress,
amount,
feeCurrencies,
sendToken,
}: {
fromWalletAddress: string
toWalletAddress: string
amount: bigint
feeCurrencies: TokenBalance[]
sendToken: NativeTokenBalance
},
prepareTxs = prepareTransactions
): Promise<PreparedTransactionsResult> {
const baseSendTx: TransactionRequest = {
from: fromWalletAddress as Address,
to: toWalletAddress as Address,
value: amount,
}
return prepareTxs({
feeCurrencies,
spendToken: sendToken,
spendTokenAmount: new BigNumber(amount.toString()),
decreasedAmountGasFeeMultiplier: 1,
baseTransactions: [baseSendTx],
})
}

/**
* Given prepared transactions, get the fee currency and amount in decimals
Expand Down
24 changes: 23 additions & 1 deletion test/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
RecipientType,
} from 'src/recipients/recipient'
import { TransactionDataInput } from 'src/send/SendAmount'
import { StoredTokenBalance, TokenBalance } from 'src/tokens/slice'
import { NativeTokenBalance, StoredTokenBalance, TokenBalance } from 'src/tokens/slice'
import { NetworkId, TokenTransactionTypeV2 } from 'src/transactions/types'
import { CiCoCurrency, Currency } from 'src/utils/currencies'
import { ONE_DAY_IN_MILLIS } from 'src/utils/time'
Expand Down Expand Up @@ -509,6 +509,20 @@ export const mockTokenBalances: Record<string, StoredTokenBalance> = {
priceFetchedAt: Date.now(),
isCashInEligible: true,
},
[mockEthTokenId]: {
priceUsd: '1500',
address: null,
tokenId: mockEthTokenId,
networkId: NetworkId['ethereum-sepolia'],
symbol: 'ETH',
imageUrl:
'https://raw.githubusercontent.com/valora-inc/address-metadata/main/assets/tokens/ETH.png',
name: 'Ether',
decimals: 18,
balance: '0',
priceFetchedAt: Date.now(),
isNative: true,
},
}

export const mockCeloTokenBalance: TokenBalance = {
Expand All @@ -518,6 +532,14 @@ export const mockCeloTokenBalance: TokenBalance = {
balance: new BigNumber(5),
}

export const mockEthTokenBalance: NativeTokenBalance = {
...mockTokenBalances[mockEthTokenId],
priceUsd: new BigNumber(1500),
lastKnownPriceUsd: new BigNumber(1500),
balance: new BigNumber(0.1),
isNative: true,
}

export const mockTokenBalancesWithHistoricalPrices = {
[mockPoofTokenId]: {
...mockTokenBalances[mockPoofTokenId],
Expand Down

0 comments on commit 8448cca

Please sign in to comment.