Skip to content

Commit

Permalink
Merge pull request #5273 from leather-wallet/release/ledger-inscriptions
Browse files Browse the repository at this point in the history
Release/ledger inscriptions
  • Loading branch information
fbwoolf authored Apr 20, 2024
2 parents b3078db + 6022122 commit 469c5e9
Show file tree
Hide file tree
Showing 14 changed files with 157 additions and 85 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@ Leather is the most popular and trusted wallet for apps built on Bitcoin. Connec

[📩 Join the mailing list for updates →](https://forms.gle/sdZPu2jbX1AeQ8Fi9)

### Contibuting

Please see our [contribution guide](.github/CONTRIBUTING.md)

## Development

This application is a browser extension. There is no ability to run it as a standalone web application.

Each child of the `src` directory represents the JavaScript context in which it is ran.

### Install packages

```bash
pnpm i
```

### Dev mode

When working on the extension, you can run it in `development` mode which will watch for any file changes and
Expand Down
65 changes: 32 additions & 33 deletions src/app/common/transactions/bitcoin/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import BigNumber from 'bignumber.js';
import { AddressType, getAddressInfo, validate } from 'bitcoin-address-validation';
import {
type AddressInfo,
AddressType,
getAddressInfo,
validate,
} from 'bitcoin-address-validation';

import { BTC_P2WPKH_DUST_AMOUNT } from '@shared/constants';
import {
Expand Down Expand Up @@ -207,46 +212,40 @@ export function getSizeInfoMultipleRecipients(payload: {
}) {
const { inputLength, recipients, isSendMax } = payload;

const addressesInfo = recipients.map(recipient => {
return validate(recipient.address) ? getAddressInfo(recipient.address) : null;
});
const outputAddressesTypesWithFallback = addressesInfo.map(addressInfo =>
addressInfo ? addressInfo.type : AddressType.p2wpkh
);

const outputTypesLengthMap = outputAddressesTypesWithFallback.reduce(
(acc: Record<AddressType, number>, outputType) => {
// we add 1 output for change address if not sending max
if (!acc['p2wpkh'] && !isSendMax) {
acc['p2wpkh'] = 1;
}

if (acc[outputType]) {
acc[outputType] = acc[outputType] + 1;
} else {
acc[outputType] = 1;
}

const validAddressesInfo = recipients
.map(recipient => validate(recipient.address) && getAddressInfo(recipient.address))
.filter(Boolean) as AddressInfo[];

function getTxOutputsLengthByPaymentType() {
return validAddressesInfo.reduce(
(acc, { type }) => {
acc[type] = (acc[type] || 0) + 1;
return acc;
},
{} as Record<AddressType, number>
);
}

const outputTypesCount = getTxOutputsLengthByPaymentType();

// Add a change address if not sending max (defaults to p2wpkh)
if (!isSendMax) {
outputTypesCount[AddressType.p2wpkh] = (outputTypesCount[AddressType.p2wpkh] || 0) + 1;
}

// Prepare the output data map for consumption by the txSizer
const outputsData = Object.entries(outputTypesCount).reduce(
(acc, [type, count]) => {
acc[type + '_output_count'] = count;
return acc;
},
{} as Record<AddressType, number>
);

const outputsData = (Object.keys(outputTypesLengthMap) as AddressType[]).map(
outputAddressType => {
return {
[outputAddressType + '_output_count']: outputTypesLengthMap[outputAddressType],
};
}
{} as Record<string, number>
);

const txSizer = new BtcSizeFeeEstimator();
const sizeInfo = txSizer.calcTxSize({
// Only p2wpkh is supported by the wallet
input_script: 'p2wpkh',
input_count: inputLength,
// From the address of the recipient, we infer the output type

...outputsData,
});

Expand Down
6 changes: 4 additions & 2 deletions src/app/components/bitcoin-fees-list/bitcoin-fees-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Stack } from 'leather-styles/jsx';

import { BtcFeeType } from '@shared/models/fees/bitcoin-fees.model';

import { LoadingSpinner } from '../loading-spinner';
import { FeesListError } from './components/fees-list-error';
import { FeesListItem } from './components/fees-list-item';

Expand All @@ -25,13 +26,15 @@ export interface OnChooseFeeArgs {

interface BitcoinFeesListProps {
feesList: FeesListItem[];
isLoading: boolean;
onChooseFee({ feeRate, feeValue, time }: OnChooseFeeArgs): Promise<void>;
onSetSelectedFeeType(value: BtcFeeType): void;
onValidateBitcoinSpend(value: number): boolean;
selectedFeeType: BtcFeeType | null;
}
export function BitcoinFeesList({
feesList,
isLoading,
onChooseFee,
onSetSelectedFeeType,
onValidateBitcoinSpend,
Expand All @@ -47,8 +50,7 @@ export function BitcoinFeesList({
[onChooseFee, onSetSelectedFeeType, onValidateBitcoinSpend]
);

// TODO: This should be changed when custom fees are implemented. We can simply
// force custom fee setting when api requests fail and we can't calculate fees.
if (isLoading) return <LoadingSpinner />;
if (!feesList.length) return <FeesListError />;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,17 @@ import { Box } from 'leather-styles/jsx';

import { RouteUrls } from '@shared/route-urls';

import { GenericError, GenericErrorListItem } from '@app/components/generic-error/generic-error';
import { GenericError } from '@app/components/generic-error/generic-error';

const body = 'Check balance and try again';
const helpTextList = [<GenericErrorListItem key={1} text="Possibly caused by api issues" />];
const body = 'Please set a custom fee';
const title = 'Unable to calculate fees';

export function FeesListError() {
const navigate = useNavigate();

return (
<Box textAlign="center" px={['unset', 'space.05']} py="space.04" width="100%">
<GenericError
body={body}
helpTextList={helpTextList}
onClose={() => navigate(RouteUrls.SendCryptoAsset)}
title={title}
/>
<GenericError body={body} onClose={() => navigate(RouteUrls.SendCryptoAsset)} title={title} />
</Box>
);
}
52 changes: 27 additions & 25 deletions src/app/components/generic-error/generic-error.layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const supportUrl =

interface GenericErrorProps extends FlexProps {
body: string;
helpTextList: ReactNode[];
helpTextList?: ReactNode[];
onClose(): void;
title: string;
}
Expand All @@ -34,30 +34,32 @@ export function GenericErrorLayout(props: GenericErrorProps) {
>
{body}
</styled.h2>
<styled.ul
border="default"
borderRadius="sm"
fontSize="14px"
lineHeight="1.6"
listStyleType="circle"
mt="space.06"
pb="space.05"
pl="40px"
pr="space.05"
pt="space.02"
textAlign="left"
width="100%"
>
{helpTextList}
<styled.li mt="space.04" textAlign="left">
<HStack alignItems="center">
<styled.span textStyle="label.02">Reach out to our support team</styled.span>
<styled.button onClick={() => openInNewTab(supportUrl)} type="button">
<ExternalLinkIcon />
</styled.button>
</HStack>
</styled.li>
</styled.ul>
{helpTextList && (
<styled.ul
border="default"
borderRadius="sm"
fontSize="14px"
lineHeight="1.6"
listStyleType="circle"
mt="space.06"
pb="space.05"
pl="40px"
pr="space.05"
pt="space.02"
textAlign="left"
width="100%"
>
{helpTextList}
<styled.li mt="space.04" textAlign="left">
<HStack alignItems="center">
<styled.span textStyle="label.02">Reach out to our support team</styled.span>
<styled.button onClick={() => openInNewTab(supportUrl)} type="button">
<ExternalLinkIcon />
</styled.button>
</HStack>
</styled.li>
</styled.ul>
)}
<Link fontSize="14px" mt="space.05" onClick={onClose}>
Close window
</Link>
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/generic-error/generic-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function GenericErrorListItem({ text }: { text: ReactNode }) {

interface GenericErrorProps extends FlexProps {
body: string;
helpTextList: ReactNode[];
helpTextList?: ReactNode[];
onClose?(): void;
title: string;
}
Expand Down
6 changes: 6 additions & 0 deletions src/app/features/bitcoin-choose-fee/bitcoin-choose-fee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { InsufficientBalanceError } from './components/insufficient-balance-erro

interface BitcoinChooseFeeProps extends FlexProps {
amount: Money;
defaultToCustomFee: boolean;
feesList: React.JSX.Element;
isLoading: boolean;
isSendingMax: boolean;
Expand All @@ -37,6 +38,7 @@ interface BitcoinChooseFeeProps extends FlexProps {
}
export function BitcoinChooseFee({
amount,
defaultToCustomFee,
feesList,
isLoading,
isSendingMax,
Expand Down Expand Up @@ -71,6 +73,7 @@ export function BitcoinChooseFee({
<ChooseFeeSubtitle isSendingMax={isSendingMax} />
)}
<ChooseFeeTabs
defaultToCustomFee={defaultToCustomFee}
customFee={
<BitcoinCustomFee
amount={amount.amount.toNumber()}
Expand All @@ -97,6 +100,7 @@ export function BitcoinChooseFee({

interface BitcoinChooseFeeMultipleRecipientsProps extends FlexProps {
amount: Money;
defaultToCustomFee: boolean;
feesList: React.JSX.Element;
isLoading: boolean;
isSendingMax: boolean;
Expand All @@ -110,6 +114,7 @@ interface BitcoinChooseFeeMultipleRecipientsProps extends FlexProps {
}
export function BitcoinChooseFeeMultipleRecipients({
amount,
defaultToCustomFee,
feesList,
isLoading,
isSendingMax,
Expand Down Expand Up @@ -144,6 +149,7 @@ export function BitcoinChooseFeeMultipleRecipients({
<ChooseFeeSubtitle isSendingMax={isSendingMax} />
)}
<ChooseFeeTabs
defaultToCustomFee={defaultToCustomFee}
customFee={
<BitcoinCustomFeeMultipleRecipients
amount={amount.amount.toNumber()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ enum CustomFeeTabs {

interface ChooseFeeTabsProps extends StackProps {
customFee: React.JSX.Element;
defaultToCustomFee: boolean;
feesList: React.JSX.Element;
}
export function ChooseFeeTabs(props: ChooseFeeTabsProps) {
const { feesList, customFee, ...rest } = props;
const { customFee, defaultToCustomFee, feesList, ...rest } = props;
const analytics = useAnalytics();

return (
<Stack flexGrow={1} gap="space.04" mt="space.02" width="100%" {...rest}>
<Tabs.Root
defaultValue={CustomFeeTabs.Recommended}
defaultValue={defaultToCustomFee ? CustomFeeTabs.Custom : CustomFeeTabs.Recommended}
onValueChange={tab => void analytics.page('view', 'custom-fee-tab-' + tab)}
>
<Tabs.List>
Expand Down
Loading

0 comments on commit 469c5e9

Please sign in to comment.