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

feat: redesign send rpc transfer flow #6000

Merged
merged 1 commit into from
Dec 17, 2024
Merged
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@
"@leather.io/query": "2.26.1",
"@leather.io/stacks": "1.4.0",
"@leather.io/tokens": "0.12.1",
"@leather.io/ui": "1.39.0",
"@leather.io/utils": "0.21.1",
"@leather.io/ui": "1.44.2",
"@ledgerhq/hw-transport-webusb": "6.27.19",
"@noble/hashes": "1.5.0",
"@noble/secp256k1": "2.1.0",
Expand Down Expand Up @@ -268,7 +268,7 @@
"@btckit/types": "0.0.19",
"@chromatic-com/storybook": "3.2.2",
"@leather.io/eslint-config": "0.7.0",
"@leather.io/panda-preset": "0.5.3",
"@leather.io/panda-preset": "0.8.0",
"@leather.io/prettier-config": "0.6.0",
"@leather.io/rpc": "2.4.0",
"@ls-lint/ls-lint": "2.2.3",
Expand Down
6,424 changes: 4,315 additions & 2,109 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions src/app/common/fees/use-fees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useEffect, useMemo, useState } from 'react';

import type { MarketData } from '@leather.io/models';

import type { RawFee } from '@app/components/bitcoin-fees-list/bitcoin-fees.utils';

export type FeeType = 'slow' | 'standard' | 'fast' | 'custom';

export interface FeeDisplayInfo {
feeType: FeeType;
baseUnitsValue: number;
feeRate: number;
titleLeft: string;
captionLeft: string;
titleRight?: string;
captionRight?: string;
}

export interface FormatFeeForDisplayArgs {
rawFee: RawFee;
marketData: MarketData;
}

interface UseFeesProps {
defaultFeeType?: FeeType;
fees: FeesRawData;
getCustomFeeData(rate: number): RawFee;
marketData: MarketData;
formatFeeForDisplay({ rawFee, marketData }: FormatFeeForDisplayArgs): FeeDisplayInfo;
}

export type FeesRawData = Record<Exclude<FeeType, 'custom'>, RawFee>;

export function useFeesHandler({
defaultFeeType = 'standard',
fees,
getCustomFeeData,
marketData,
formatFeeForDisplay,
}: UseFeesProps) {
const [selectedFeeType, setSelectedFeeType] = useState<FeeType>(defaultFeeType);
const [editFeeSelected, setEditFeeSelected] = useState<FeeType>(selectedFeeType);
const [customFeeRate, setCustomFeeRate] = useState<string>('');

const customFeeData = getCustomFeeData(Number(customFeeRate));

const selectedFeeData = useMemo(() => {
if (selectedFeeType === 'custom') {
return formatFeeForDisplay({ rawFee: customFeeData, marketData });
}

const rawFee = fees[selectedFeeType];

if (!rawFee) {
return {};
}

return formatFeeForDisplay({ rawFee, marketData });
}, [fees, selectedFeeType, customFeeData, marketData, formatFeeForDisplay]);

useEffect(() => {
if (customFeeRate === '' && selectedFeeType !== 'custom') {
const data = fees[selectedFeeType];
if (data && data.feeRate) {
setCustomFeeRate(data.feeRate.toString());
}
}
}, [fees, selectedFeeType, customFeeRate]);

return {
selectedFeeType,
setSelectedFeeType,
selectedFeeData,
editFeeSelected,
setEditFeeSelected,
customFeeRate,
setCustomFeeRate,
customFeeData,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SwitchAccountOutletContext } from './switch-account';
export function useSwitchAccountSheet() {
const { isShowingSwitchAccount, setIsShowingSwitchAccount } =
useOutletContext<SwitchAccountOutletContext>();

return {
isShowingSwitchAccount,
setIsShowingSwitchAccount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function determineUtxosForSpendAll({
throw new Error('Cannot calculate spend of invalid address type');
});
const filteredUtxos = filterUneconomicalUtxos({ utxos, feeRate, recipients });

if (!filteredUtxos.length) throw new InsufficientFundsError();
const sizeInfo = getSizeInfo({
inputLength: filteredUtxos.length,
isSendMax: true,
Expand Down
12 changes: 10 additions & 2 deletions src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ interface GenerateNativeSegwitTxValues {
recipients: TransferRecipient[];
}

export function useGenerateUnsignedNativeSegwitTx() {
interface UseGenerateUnsignedNativeSegwitTxProps {
throwError?: boolean;
}

// temp arg before refactoring all flows to new design
pete-watters marked this conversation as resolved.
Show resolved Hide resolved
export function useGenerateUnsignedNativeSegwitTx({
throwError = false,
}: UseGenerateUnsignedNativeSegwitTxProps = {}) {
const signer = useCurrentAccountNativeSegwitIndexZeroSigner();

const networkMode = useBitcoinScureLibNetworkConfig();
Expand Down Expand Up @@ -87,9 +94,10 @@ export function useGenerateUnsignedNativeSegwitTx() {
} catch (e) {
// eslint-disable-next-line no-console
console.log('Error signing bitcoin transaction', e);
if (throwError) throw e;
return null;
}
},
[networkMode, signer.address, signer.publicKey]
[networkMode, signer.address, signer.publicKey, throwError]
);
}
16 changes: 16 additions & 0 deletions src/app/components/account/account-bitcoin-address.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Caption } from '@leather.io/ui';
import { truncateMiddle } from '@leather.io/utils';

import { BitcoinNativeSegwitAccountLoader } from '../loaders/bitcoin-account-loader';

interface AccountBitcoinAddressProps {
index: number;
}

export function AccountBitcoinAddress({ index }: AccountBitcoinAddressProps) {
return (
<BitcoinNativeSegwitAccountLoader index={index}>
{signer => <Caption>{truncateMiddle(signer.address, 4)}</Caption>}
</BitcoinNativeSegwitAccountLoader>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { styled } from 'leather-styles/jsx';

import { Caption, ItemLayout, Pressable, SkeletonLoader } from '@leather.io/ui';
import { formatDustUsdAmounts, formatMoneyPadded, i18nFormatCurrency } from '@leather.io/utils';

import { useAccountDisplayName } from '@app/common/hooks/account/use-account-names';
import { useConvertCryptoCurrencyToFiatAmount } from '@app/common/hooks/use-convert-to-fiat-amount';
import { useCurrentBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useStacksAccounts } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { AccountAvatarItem } from '@app/ui/components/account/account-avatar/account-avatar-item';

import { AccountBitcoinAddress } from '../account/account-bitcoin-address';
import { AccountNameLayout } from '../account/account-name';
import { ApproveTransactionSwitchAccount } from './approve-transaction-switch-account';

interface ApproveBitcoinTransactionSwitchAccountProps {
toggleSwitchAccount(): void;
}

export function ApproveBitcoinTransactionSwitchAccount({
toggleSwitchAccount,
}: ApproveBitcoinTransactionSwitchAccountProps) {
const index = useCurrentAccountIndex();
const stacksAccounts = useStacksAccounts();
const { balance, isLoading: isLoadingBalance } = useCurrentBtcCryptoAssetBalanceNativeSegwit();

const convertToFiatAmount = useConvertCryptoCurrencyToFiatAmount('BTC');
const fiatAmount = convertToFiatAmount(balance.availableBalance);

const stxAddress = stacksAccounts[index]?.address || '';
const { data: name = '', isLoading: isLoadingName } = useAccountDisplayName({
address: stxAddress,
index,
});

const titleRight = (
<SkeletonLoader isLoading={isLoadingBalance} width="96px">
<styled.span textStyle="label.02">{formatMoneyPadded(balance.availableBalance)}</styled.span>
</SkeletonLoader>
);

const captionRight = (
<SkeletonLoader isLoading={isLoadingBalance} width="48px">
<Caption>{formatDustUsdAmounts(i18nFormatCurrency(fiatAmount))}</Caption>
</SkeletonLoader>
);

return (
<ApproveTransactionSwitchAccount>
<Pressable onClick={toggleSwitchAccount}>
<ItemLayout
showChevron
img={<AccountAvatarItem index={0} publicKey="" name="" />}
titleLeft={<AccountNameLayout isLoading={isLoadingName}>{name}</AccountNameLayout>}
captionLeft={<AccountBitcoinAddress index={index} />}
titleRight={titleRight}
captionRight={captionRight}
/>
</Pressable>
</ApproveTransactionSwitchAccount>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { FormError } from '@app/components/error/form-error';

interface ApproveTransactionErrorProps {
isLoading: boolean;
isInsufficientBalance: boolean;
}

export function ApproveTransactionError({
isLoading,
isInsufficientBalance,
}: ApproveTransactionErrorProps) {
if (isLoading) return null;
if (isInsufficientBalance) return <FormError text="Available balance insufficient" />;
return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { styled } from 'leather-styles/jsx';

import { Approver, QuestionCircleIcon } from '@leather.io/ui';

interface ApproveTransactionHeaderProps {
title: string;
href?: string;
onPressRequestedByLink(e: React.MouseEvent<HTMLAnchorElement>): void;
}

export function ApproveTransactionHeader({
title,
href = 'https://leather.io/guides/connect-dapps',
onPressRequestedByLink,
}: ApproveTransactionHeaderProps) {
return (
<Approver.Header
title={title}
info={
<styled.a display="block" p="space.01" target="_blank" href={href}>
<QuestionCircleIcon variant="small" />
</styled.a>
}
onPressRequestedByLink={onPressRequestedByLink}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { HStack, styled } from 'leather-styles/jsx';

import { AddressDisplayer, Approver, BtcAvatarIcon, ItemLayout, UserIcon } from '@leather.io/ui';
import { formatDustUsdAmounts, formatMoneyPadded, i18nFormatCurrency } from '@leather.io/utils';

import type { TransferRecipient } from '@shared/models/form.model';

import { useConvertCryptoCurrencyToFiatAmount } from '@app/common/hooks/use-convert-to-fiat-amount';
import { IconWrapper } from '@app/components/icon-wrapper';
import { Divider } from '@app/components/layout/divider';

interface ApproveTransactionRecipientsProps {
recipients: TransferRecipient[];
}

export function ApproveTransactionRecipients({ recipients }: ApproveTransactionRecipientsProps) {
const convertToFiatAmount = useConvertCryptoCurrencyToFiatAmount('BTC');

return recipients.map(({ address, amount }) => {
const fiatAmount = convertToFiatAmount(amount);

const titleRight = formatMoneyPadded(amount);
const captionRight = formatDustUsdAmounts(i18nFormatCurrency(fiatAmount));

return (
<Approver.Section key={address}>
<Approver.Subheader>
<styled.span textStyle="label.01">You'll send</styled.span>
</Approver.Subheader>

<ItemLayout
img={<BtcAvatarIcon />}
titleLeft="Bitcoin"
captionLeft="Bitcoin blockchain"
titleRight={titleRight}
captionRight={captionRight}
/>

<Divider mt="space.05" mb="space.04" />

<Approver.Subheader>
<styled.span textStyle="label.01">To address</styled.span>
</Approver.Subheader>
<HStack key={address} alignItems="center" gap="space.04">
<IconWrapper>
<UserIcon />
</IconWrapper>
<AddressDisplayer address={address} />
</HStack>
</Approver.Section>
);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Approver, ItemLayout, Pressable } from '@leather.io/ui';

import type { FeeDisplayInfo } from '@app/common/fees/use-fees';

import { CryptoAssetItemPlaceholder } from '../crypto-asset-item/crypto-asset-item-placeholder';
import { FeeItemIcon } from '../fees/fee-item-icon';

interface ApproveTransactionSelectedFeeProps {
isLoading: boolean;
selectedFeeData: FeeDisplayInfo;
onChooseTransferFee(): void;
}

export function ApproveTransactionSelectedFee({
isLoading,
selectedFeeData,
onChooseTransferFee,
}: ApproveTransactionSelectedFeeProps) {
return (
<Approver.Section>
<Approver.Subheader>Fee</Approver.Subheader>
<Pressable onClick={onChooseTransferFee} mb="space.02">
{isLoading || !selectedFeeData ? (
<CryptoAssetItemPlaceholder my="0" />
) : (
<ItemLayout
img={<FeeItemIcon feeType={selectedFeeData.feeType} />}
titleLeft={selectedFeeData.titleLeft}
captionLeft={selectedFeeData.captionLeft}
titleRight={selectedFeeData.titleRight}
captionRight={selectedFeeData.captionRight}
showChevron
/>
)}
</Pressable>
</Approver.Section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Box } from 'leather-styles/jsx';

import { Approver } from '@leather.io/ui';

import type { HasChildren } from '@app/common/has-children';

export function ApproveTransactionSwitchAccount({ children }: HasChildren) {
return (
<Approver.Section>
<Approver.Subheader>With account</Approver.Subheader>
<Box mb="space.03">{children}</Box>
</Approver.Section>
);
}
Loading
Loading