Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/frontend/src/eth/services/eth-transaction.services.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { enabledEthEvmNativeTokens } from '$eth/derived/native-tokens.derived';
import { alchemyProviders } from '$eth/providers/alchemy.providers';
import { reloadEthereumBalance } from '$eth/services/eth-balance.services';
import { reloadEthereumTransactions } from '$eth/services/eth-transactions.services';
Expand Down Expand Up @@ -142,6 +143,22 @@ const processMinedTransaction = async ({

await reloadEthereumTransactions({ identity, tokenId, networkId, chainId, standard });

// Reload balance as a transaction has been mined
await reloadEthereumBalance(token);
// Reload balance as a transaction has been mined.
//
// The gas is paid in the network's native coin whatever the transaction moved, so an ERC-20
// transfer leaves that balance overstated as well, not only the balance of the token it sent.
// Nothing else corrects it until the next balance poll, and in that window "Max" prices a native
// send against a balance that still holds the gas already spent: the amount plus the gas it
// reserves exceeds what the account has, and the chain rejects the transaction outright rather
// than trimming it.
const nativeToken = get(enabledEthEvmNativeTokens).find(
({ network: { id } }) => id === networkId
);

const tokensToReload: Token[] = [
token,
...(nonNullish(nativeToken) && nativeToken.id !== tokenId ? [nativeToken] : [])
];

await Promise.all(tokensToReload.map((tokenToReload) => reloadEthereumBalance(tokenToReload)));
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ETHEREUM_NETWORK } from '$env/networks/networks.eth.env';
import { ETHEREUM_TOKEN, SEPOLIA_TOKEN } from '$env/tokens/tokens.eth.env';
import * as nativeTokensDerived from '$eth/derived/native-tokens.derived';
import * as ethBalanceServices from '$eth/services/eth-balance.services';
import { processErc20Transaction } from '$eth/services/eth-transaction.services';
import * as ethTransactionsServices from '$eth/services/eth-transactions.services';
import type { RequiredToken } from '$lib/types/token';
import { mockValidErc20Token } from '$tests/mocks/erc20-tokens.mock';
import { mockIdentity } from '$tests/mocks/identity.mock';
import { readable } from 'svelte/store';

describe('eth-transaction.services', () => {
describe('processErc20Transaction', () => {
const hash = '0x5f97b634e4173d1b167c23386b677976a8d807b91eefe9e407b0025cff4fe441';

const mockNativeTokens = (tokens: RequiredToken[]) =>
vi
.spyOn(nativeTokensDerived, 'enabledEthEvmNativeTokens', 'get')
.mockReturnValue(readable(tokens));

const processMined = (token: Parameters<typeof processErc20Transaction>[0]['token']) =>
processErc20Transaction({
identity: mockIdentity,
hash,
value: 1n,
token,
type: 'mined'
});

beforeEach(() => {
vi.clearAllMocks();

vi.spyOn(ethTransactionsServices, 'reloadEthereumTransactions').mockResolvedValue({
success: true
});
vi.spyOn(ethBalanceServices, 'reloadEthereumBalance').mockResolvedValue({ success: true });

mockNativeTokens([ETHEREUM_TOKEN as RequiredToken]);
});

it('reloads the native balance too, since the mined transfer paid its gas out of it', async () => {
await processMined(mockValidErc20Token);

expect(ethBalanceServices.reloadEthereumBalance).toHaveBeenCalledTimes(2);

expect(ethBalanceServices.reloadEthereumBalance).toHaveBeenCalledWith(mockValidErc20Token);

expect(ethBalanceServices.reloadEthereumBalance).toHaveBeenCalledWith(ETHEREUM_TOKEN);
});

it('reloads the balance once when the token that was sent is the native one', async () => {
await processMined(ETHEREUM_TOKEN);

expect(ethBalanceServices.reloadEthereumBalance).toHaveBeenCalledExactlyOnceWith(
ETHEREUM_TOKEN
);
});

it('reloads only the token that was sent when no native token matches its network', async () => {
mockNativeTokens([SEPOLIA_TOKEN as RequiredToken]);

await processMined(mockValidErc20Token);

expect(ethBalanceServices.reloadEthereumBalance).toHaveBeenCalledExactlyOnceWith(
mockValidErc20Token
);
});

it('reloads the transactions of the token that was sent', async () => {
await processMined(mockValidErc20Token);

expect(ethTransactionsServices.reloadEthereumTransactions).toHaveBeenCalledExactlyOnceWith({
identity: mockIdentity,
tokenId: mockValidErc20Token.id,
networkId: ETHEREUM_NETWORK.id,
chainId: ETHEREUM_NETWORK.chainId,
standard: mockValidErc20Token.standard
});
});
});
});
Loading