-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(frontend): BtcConvertForm component (#3650)
# 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
1 parent
75c80b0
commit 7a6c395
Showing
3 changed files
with
244 additions
and
1 deletion.
There are no files selected for viewing
82 changes: 82 additions & 0 deletions
82
src/frontend/src/btc/components/convert/BtcConvertForm.svelte
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,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> |
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
161 changes: 161 additions & 0 deletions
161
src/frontend/src/tests/btc/components/convert/BtcConvertForm.spec.ts
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,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 | ||
); | ||
}); | ||
}); | ||
}); |