Skip to content
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

[LW 9904] "UTxO Fully Depleted" error in send flow #1116

Merged
merged 25 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b253da3
feat: add error banner
Apr 30, 2024
d734813
feat: update copy
May 2, 2024
4da1156
feat: display max button when utxo depleted error is present
May 2, 2024
780c2f7
feat: max ada is value that doesnt throw utxo depleted error
May 9, 2024
89dd76c
feat: only display max button suggestion if there is spendable amount
May 13, 2024
5c69e8d
fix: add margin
May 22, 2024
3bbdd79
feat: recalculate max ada value on output changes
May 22, 2024
a47e4f0
fix: only add metadata if not empty
May 27, 2024
c5532a2
fix: round spendable value down
May 27, 2024
ac0766c
fix: only call useMaxAda once
May 27, 2024
0a1c9fc
refactor: debounce for output changes re renders
May 27, 2024
f0cf533
fix: update tests
May 28, 2024
e7b43ef
fix: use max amount on first output
May 29, 2024
9397230
fix: consider rewards on send flow max amount allowed
May 29, 2024
ffa28bd
feat: disable button while max ada is calculated
May 29, 2024
eeba881
Merge branch 'main' into feat/lw-9904-utxo-fully-depleted-error-in-se…
Jun 4, 2024
b18b93a
test(extension): fix for smoke tests
ljagiela Jun 4, 2024
6f39ac0
Merge branch 'main' into feat/lw-9904-utxo-fully-depleted-error-in-se…
ljagiela Jun 14, 2024
a172f71
test(extension): e2e test adjustments
ljagiela Jun 14, 2024
19225a0
Merge branch 'main' into feat/lw-9904-utxo-fully-depleted-error-in-se…
Jun 18, 2024
bdb54e3
fix: merge conflict
Jun 18, 2024
eae325a
fix: unfocus input before clicking button
Jun 18, 2024
868f2f3
fix: default address if not present yet
Jun 19, 2024
7bb3a07
feat: increment test
Jun 19, 2024
f222985
test(extension): add test automation fixes for sending objects
bslabiak Jun 20, 2024
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
1 change: 1 addition & 0 deletions apps/browser-extension-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@lace/cardano": "0.1.0",
"@lace/common": "0.1.0",
"@lace/core": "0.1.0",
"@lace/icons": "workspace:^",
"@lace/shared-wallets": "workspace:^",
"@lace/staking": "0.1.0",
"@lace/translation": "0.1.0",
Expand Down
117 changes: 103 additions & 14 deletions apps/browser-extension-wallet/src/hooks/__tests__/useMaxAda.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable no-magic-numbers */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useMaxAda } from '../useMaxAda';
import { useMaxAda, UTXO_DEPLETED_ADA_BUFFER } from '../useMaxAda';
import { renderHook } from '@testing-library/react-hooks';
import { mockWalletInfoTestnet } from '@src/utils/mocks/test-helpers';
import { Subject, of } from 'rxjs';
import { Wallet } from '@lace/cardano';
import { waitFor } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
const mockInitializeTx = jest.fn();

const MIN_COINS_FOR_TOKENS = 1_155_080;
const TX_FEE = 155_381;

const inspect = jest.fn();
const mockCreateTxBuilder = jest.fn().mockReturnValue({
inspect,
build: jest.fn().mockReturnThis(),
addOutput: jest.fn().mockReturnThis(),
removeOutput: jest.fn().mockReturnThis()
});
const inMemoryWallet = {
balance: {
utxo: {
Expand All @@ -18,7 +29,7 @@ const inMemoryWallet = {
}
},
protocolParameters$: of({ coinsPerUtxoByte: 4310, maxValueSize: 5000 }),
initializeTx: mockInitializeTx
createTxBuilder: mockCreateTxBuilder
};

jest.mock('../../stores', () => ({
Expand All @@ -30,17 +41,26 @@ jest.mock('../../stores', () => ({
})
}));

const outputMap = new Map();

jest.mock('../../views/browser-view/features/send-transaction', () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...jest.requireActual<any>('../../views/browser-view/features/send-transaction'),
useTransactionProps: () => ({
outputMap
})
}));

describe('Testing useMaxAda hook', () => {
beforeEach(() => {
mockInitializeTx.mockImplementationOnce(() => ({
inspect.mockResolvedValue({
inputSelection: {
// eslint-disable-next-line no-magic-numbers
fee: BigInt(155_381)
fee: BigInt(TX_FEE)
}
}));
});
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});

test('should return 0 in case balance is empty', async () => {
Expand All @@ -60,8 +80,8 @@ describe('Testing useMaxAda hook', () => {
});

test('should return 0 in case there is an error', async () => {
mockInitializeTx.mockReset();
mockInitializeTx.mockImplementation(async () => {
inspect.mockReset();
inspect.mockImplementation(async () => {
throw new Error('init tx error');
});
const { result } = renderHook(() => useMaxAda());
Expand All @@ -74,19 +94,39 @@ describe('Testing useMaxAda hook', () => {
});
});

test('should return 7874869', async () => {
test('should return 0 if balance is minimum for coins', async () => {
const { result } = renderHook(() => useMaxAda());
act(() => {
inMemoryWallet.balance.utxo.available$.next({
coins: BigInt(MIN_COINS_FOR_TOKENS) + BigInt(TX_FEE),
assets: new Map([
[
Wallet.Cardano.AssetId('659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41'),
BigInt('100000000')
]
])
});
});

await waitFor(() => {
expect(result.current.toString()).toBe('0');
});
});

test('should return balance minus fee', async () => {
const { result } = renderHook(() => useMaxAda());

act(() => {
inMemoryWallet.balance.utxo.available$.next({ coins: BigInt('10000000') });
});
await waitFor(() => {
expect(result.current.toString()).toBe('7874869');
expect(result.current).toBe(BigInt('10000000') - BigInt(TX_FEE));
});
});

test('should return 7689539', async () => {
test('should return balance minus fee and minimun ada for tokens', async () => {
const { result } = renderHook(() => useMaxAda());

act(() => {
inMemoryWallet.balance.utxo.available$.next({
coins: BigInt('10000000'),
Expand All @@ -98,9 +138,58 @@ describe('Testing useMaxAda hook', () => {
])
});
});
await waitFor(() => {
expect(result.current).toBe(BigInt('10000000') - BigInt(TX_FEE) - BigInt(MIN_COINS_FOR_TOKENS));
});
});

test.each([1, 2, 3, 10])('should return balance minus fee and adaErrorBuffer times %i', async (errorCount) => {
inspect.mockResolvedValueOnce({
inputSelection: {
fee: BigInt(TX_FEE)
}
});
Array.from({ length: errorCount }).forEach(() => {
inspect.mockImplementationOnce(() => {
throw new Error('Error');
});
});

const { result } = renderHook(() => useMaxAda());

act(() => {
inMemoryWallet.balance.utxo.available$.next({
coins: BigInt('20000000')
});
});

await waitFor(() => {
expect(result.current).toBe(BigInt('20000000') - BigInt(TX_FEE) - BigInt(UTXO_DEPLETED_ADA_BUFFER * errorCount));
});
});

test('should return balance minus fee and adaErrorBuffer times %i', async () => {
inspect.mockResolvedValueOnce({
inputSelection: {
fee: BigInt(TX_FEE)
}
});
Array.from({ length: 11 }).forEach(() => {
inspect.mockImplementationOnce(() => {
throw new Error('Error');
});
});

const { result } = renderHook(() => useMaxAda());

act(() => {
inMemoryWallet.balance.utxo.available$.next({
coins: BigInt('20000000')
});
});

await waitFor(() => {
expect(result.current.toString()).toBe('7689539');
expect(result.current).toBe(BigInt(0));
});
});
});
20 changes: 13 additions & 7 deletions apps/browser-extension-wallet/src/hooks/useInitializeTx.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/* eslint-disable consistent-return */
import { useEffect, useCallback } from 'react';
import { Wallet } from '@lace/cardano';
import { BuiltTxData, OutputsMap } from '../views/browser-view/features/send-transaction/types';
import { useSpentBalances } from '../views/browser-view/features/send-transaction/store';
import { useObservable } from '@lace/common';
import { getReachedMaxAmountList } from '@src/views/browser-view/features/send-transaction/helpers';
import { useWalletStore } from '@src/stores';
import { useMaxAda } from './useMaxAda';
import { UseTranslationResponse } from 'react-i18next';
import type { TranslationKey } from '@lace/translation';

Expand All @@ -29,7 +29,11 @@ export const coinSelectionErrors = new Map<COIN_SELECTION_ERRORS, TranslationKey

export const getErrorMessage =
(t: UseTranslationResponse<'translation'>['t']) =>
(key: COIN_SELECTION_ERRORS): string => {
(key: COIN_SELECTION_ERRORS): string | undefined => {
if (key === COIN_SELECTION_ERRORS.FULLY_DEPLETED_ERROR) {
return;
}

if (coinSelectionErrors.has(key)) {
return t(coinSelectionErrors.get(key));
}
Expand All @@ -49,16 +53,16 @@ export const useInitializeTx = (
const assetsInfo = useObservable(inMemoryWallet.assetInfo$);
const balance = useObservable(inMemoryWallet.balance.utxo.total$);
const tokensUsed = useSpentBalances();
const spendableCoin = useMaxAda();
const availableRewards = useObservable(inMemoryWallet.balance.rewardAccounts.rewards$);

const buildTransaction = useCallback(async () => {
const reachedMaxAmountList = getReachedMaxAmountList({
assets: assetsInfo,
tokensUsed,
spendableCoin,
balance,
exceed: true,
cardanoCoin
cardanoCoin,
availableRewards
});
if (hasInvalidOutputs || reachedMaxAmountList.length > 0) {
setBuiltTxData({
Expand All @@ -78,7 +82,9 @@ export const useInitializeTx = (
const txBuilder = inMemoryWallet.createTxBuilder();

outputsWithMissingCoins.outputs.forEach((output) => txBuilder.addOutput(output));
txBuilder.metadata(partialTxProps?.auxiliaryData?.blob || new Map());
if (partialTxProps?.auxiliaryData?.blob) {
txBuilder.metadata(partialTxProps.auxiliaryData.blob);
}
const tx = txBuilder.build();
const inspection = await tx.inspect();
setBuiltTxData({
Expand Down Expand Up @@ -107,9 +113,9 @@ export const useInitializeTx = (
}, [
assetsInfo,
tokensUsed,
spendableCoin,
balance,
cardanoCoin,
availableRewards,
hasInvalidOutputs,
setBuiltTxData,
metadata,
Expand Down
Loading
Loading