Skip to content

Commit

Permalink
feat(frontend): BtcConvertForm component (#3650)
Browse files Browse the repository at this point in the history
# Motivation

BtcConvertForm component:

<img width="593" alt="Screenshot 2024-11-19 at 10 38 12"
src="https://github.com/user-attachments/assets/aa259186-2a44-4cb5-bd9e-d2588f826431">
  • Loading branch information
DenysKarmazynDFINITY authored Nov 19, 2024
1 parent 75c80b0 commit 7a6c395
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 1 deletion.
82 changes: 82 additions & 0 deletions src/frontend/src/btc/components/convert/BtcConvertForm.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script lang="ts">
import { isNullish, nonNullish } from '@dfinity/utils';
import { getContext } from 'svelte';
import type { Readable } from 'svelte/store';
import { slide } from 'svelte/transition';
import BtcConvertFeeTotal from '$btc/components/convert/BtcConvertFeeTotal.svelte';
import BtcConvertFees from '$btc/components/convert/BtcConvertFees.svelte';
import BtcSendWarnings from '$btc/components/send/BtcSendWarnings.svelte';
import {
BtcPendingSentTransactionsStatus,
initPendingSentTransactionsStatus
} from '$btc/derived/btc-pending-sent-transactions-status.derived';
import { UTXOS_FEE_CONTEXT_KEY, type UtxosFeeContext } from '$btc/stores/utxos-fee.store';
import ConvertForm from '$lib/components/convert/ConvertForm.svelte';
import Hr from '$lib/components/ui/Hr.svelte';
import MessageBox from '$lib/components/ui/MessageBox.svelte';
import { SLIDE_DURATION } from '$lib/constants/transition.constants';
import { i18n } from '$lib/stores/i18n.store';
import type { OptionAmount } from '$lib/types/send';
import { invalidAmount } from '$lib/utils/input.utils';
export let source: string;
export let sendAmount: OptionAmount;
export let receiveAmount: number | undefined;
const { store: storeUtxosFeeData } = getContext<UtxosFeeContext>(UTXOS_FEE_CONTEXT_KEY);
let insufficientFunds: boolean;
let insufficientFundsForFee: boolean;
let hasPendingTransactionsStore: Readable<BtcPendingSentTransactionsStatus>;
$: hasPendingTransactionsStore = initPendingSentTransactionsStatus(source);
let invalid: boolean;
$: invalid =
insufficientFunds ||
insufficientFundsForFee ||
invalidAmount(sendAmount) ||
$hasPendingTransactionsStore !== BtcPendingSentTransactionsStatus.NONE ||
isNullish($storeUtxosFeeData?.utxosFee?.utxos) ||
$storeUtxosFeeData.utxosFee.utxos.length === 0;
let totalFee: bigint | undefined;
</script>

<ConvertForm
on:icNext
bind:sendAmount
bind:receiveAmount
bind:insufficientFunds
bind:insufficientFundsForFee
{totalFee}
disabled={invalid}
>
<svelte:fragment slot="message">
{#if insufficientFundsForFee}
<div transition:slide={SLIDE_DURATION} data-tid="btc-convert-form-insufficient-funds-for-fee">
<MessageBox level="error"
><span class="text-error">{$i18n.convert.assertion.insufficient_funds_for_fee}</span
></MessageBox
>
</div>
{:else if nonNullish($hasPendingTransactionsStore)}
<div class="mb-4" data-tid="btc-convert-form-send-warnings">
<BtcSendWarnings
utxosFee={$storeUtxosFeeData?.utxosFee}
pendingTransactionsStatus={$hasPendingTransactionsStore}
/>
</div>
{/if}
</svelte:fragment>

<svelte:fragment slot="fee">
<BtcConvertFees {sendAmount} />

<Hr spacing="md" />

<BtcConvertFeeTotal bind:totalFee />
</svelte:fragment>

<slot name="cancel" slot="cancel" />
</ConvertForm>
2 changes: 1 addition & 1 deletion src/frontend/src/lib/components/convert/ConvertForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<ButtonGroup slot="toolbar">
<slot name="cancel" />

<Button {disabled} on:click={() => dispatch('icNext')}>
<Button {disabled} on:click={() => dispatch('icNext')} testId="convert-form-button-next">
{$i18n.convert.text.review_button}
</Button>
</ButtonGroup>
Expand Down
161 changes: 161 additions & 0 deletions src/frontend/src/tests/btc/components/convert/BtcConvertForm.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import BtcConvertForm from '$btc/components/convert/BtcConvertForm.svelte';
import * as btcPendingSendTransactionsStatusStore from '$btc/derived/btc-pending-sent-transactions-status.derived';
import {
initUtxosFeeStore,
UTXOS_FEE_CONTEXT_KEY,
type UtxosFeeStore
} from '$btc/stores/utxos-fee.store';
import { BTC_MAINNET_TOKEN } from '$env/tokens.btc.env';
import { ICP_TOKEN } from '$env/tokens.env';
import { CONVERT_CONTEXT_KEY } from '$lib/stores/convert.store';
import * as convertUtils from '$lib/utils/convert.utils';
import { mockBtcAddress, mockUtxosFee } from '$tests/mocks/btc.mock';
import en from '$tests/mocks/i18n.mock';
import { mockPage } from '$tests/mocks/page.store.mock';
import { render, waitFor } from '@testing-library/svelte';
import { BigNumber } from 'alchemy-sdk';
import { readable } from 'svelte/store';

describe('BtcConvertForm', () => {
let store: UtxosFeeStore;
const mockContext = ({
utxosFeeStore,
sourceTokenBalance = 1000000n
}: {
utxosFeeStore: UtxosFeeStore;
sourceTokenBalance?: bigint;
}) =>
new Map([
[UTXOS_FEE_CONTEXT_KEY, { store: utxosFeeStore }],
[
CONVERT_CONTEXT_KEY,
{
sourceToken: readable(BTC_MAINNET_TOKEN),
sourceTokenBalance: readable(BigNumber.from(sourceTokenBalance)),
destinationToken: readable(ICP_TOKEN)
}
]
]);
const props = {
source: mockBtcAddress,
sendAmount: 0.001,
receiveAmount: 0.001
};
const mockBtcPendingSendTransactionsStatusStore = (
status:
| btcPendingSendTransactionsStatusStore.BtcPendingSentTransactionsStatus
| undefined = btcPendingSendTransactionsStatusStore.BtcPendingSentTransactionsStatus.NONE
) =>
vi
.spyOn(btcPendingSendTransactionsStatusStore, 'initPendingSentTransactionsStatus')
.mockImplementation(() => readable(status));

const buttonTestId = 'convert-form-button-next';
const insufficientFundsForFeeTestId = 'btc-convert-form-insufficient-funds-for-fee';
const btcSendWarningsTestId = 'btc-convert-form-send-warnings';

beforeEach(() => {
mockPage.reset();
store = initUtxosFeeStore();
store.reset();
});

it('should keep the next button clickable if all requirements are met', () => {
store.setUtxosFee({ utxosFee: mockUtxosFee });
mockBtcPendingSendTransactionsStatusStore();

const { getByTestId } = render(BtcConvertForm, {
props,
context: mockContext({ utxosFeeStore: store })
});

expect(getByTestId(buttonTestId)).not.toHaveAttribute('disabled');
});

it('should keep the next button disabled if amount is undefined', () => {
store.setUtxosFee({ utxosFee: mockUtxosFee });
mockBtcPendingSendTransactionsStatusStore();

const { getByTestId } = render(BtcConvertForm, {
props: {
...props,
sendAmount: undefined
},
context: mockContext({ utxosFeeStore: store })
});

expect(getByTestId(buttonTestId)).toHaveAttribute('disabled');
});

it('should keep the next button disabled if amount is invalid', () => {
store.setUtxosFee({ utxosFee: mockUtxosFee });
mockBtcPendingSendTransactionsStatusStore();

const { getByTestId } = render(BtcConvertForm, {
props: {
...props,
sendAmount: -1
},
context: mockContext({ utxosFeeStore: store })
});

expect(getByTestId(buttonTestId)).toHaveAttribute('disabled');
});

it('should keep the next button disabled if utxos are undefined', () => {
mockBtcPendingSendTransactionsStatusStore();

const { getByTestId } = render(BtcConvertForm, {
props,
context: mockContext({ utxosFeeStore: store })
});

expect(getByTestId(buttonTestId)).toHaveAttribute('disabled');
});

it('should keep the next button disabled if utxos are not available', () => {
store.setUtxosFee({ utxosFee: { ...mockUtxosFee, utxos: [] } });
mockBtcPendingSendTransactionsStatusStore();

const { getByTestId } = render(BtcConvertForm, {
props,
context: mockContext({ utxosFeeStore: store })
});

expect(getByTestId(buttonTestId)).toHaveAttribute('disabled');
});

it('should render insufficient funds for fee message', async () => {
vi.spyOn(convertUtils, 'validateConvertAmount').mockImplementation(
() => 'insufficient-funds-for-fee'
);

const { getByTestId } = render(BtcConvertForm, {
props,
context: mockContext({ utxosFeeStore: store, sourceTokenBalance: 0n })
});

await waitFor(() => {
expect(getByTestId(insufficientFundsForFeeTestId)).toHaveTextContent(
en.convert.assertion.insufficient_funds_for_fee
);
});
});

it('should render btc send warning message', async () => {
mockBtcPendingSendTransactionsStatusStore(
btcPendingSendTransactionsStatusStore.BtcPendingSentTransactionsStatus.SOME
);

const { getByTestId } = render(BtcConvertForm, {
props,
context: mockContext({ utxosFeeStore: store })
});

await waitFor(() => {
expect(getByTestId(btcSendWarningsTestId)).toHaveTextContent(
en.send.info.pending_bitcoin_transaction
);
});
});
});

0 comments on commit 7a6c395

Please sign in to comment.