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

fix(transfer): remove 80% onchain cap, misc fixes #2406

Merged
merged 1 commit into from
Dec 18, 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
2 changes: 1 addition & 1 deletion e2e/channels.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
let { label } = await element(
by.id('SpendingAdvancedNumberField'),
).getAttributes();
const lspBalance = Number.parseInt(label);

Check warning on line 110 in e2e/channels.e2e.js

View workflow job for this annotation

GitHub Actions / Run lint check

Missing radix parameter
jestExpect(lspBalance).toBeGreaterThan(440);
jestExpect(lspBalance).toBeLessThan(460);
await element(by.id('SpendingAdvancedNumberField')).tap();
Expand Down Expand Up @@ -160,7 +160,7 @@
// Receiving Capacity
// can continue with min amount
await element(by.id('SpendingAdvancedMin')).tap();
await expect(element(by.text('2 000'))).toBeVisible();
await expect(element(by.text('2 500'))).toBeVisible();
await element(by.id('SpendingAdvancedContinue')).tap();
await element(by.id('SpendingConfirmDefault')).tap();
await element(by.id('SpendingConfirmAdvanced')).tap();
Expand Down
72 changes: 54 additions & 18 deletions src/hooks/transfer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useEffect, useState } from 'react';

import { useAppSelector } from './redux';
import { onChainBalanceSelector } from '../store/reselect/wallet';
import { estimateOrderFee } from '../utils/blocktank';
import { fiatToBitcoinUnit } from '../utils/conversion';
import { blocktankInfoSelector } from '../store/reselect/blocktank';
import { blocktankChannelsSizeSelector } from '../store/reselect/lightning';
import { fiatToBitcoinUnit } from '../utils/conversion';

type TTransferValues = {
maxClientBalance: number;
Expand Down Expand Up @@ -36,25 +38,18 @@ const getMinLspBalance = (
clientBalance: number,
minChannelSize: number,
): number => {
// LSP balance must be at least 2% of the channel size for LDK to accept (reserve balance)
const ldkMinimum = Math.round(clientBalance * 0.02);
// LSP balance must be at least 2.5% of the channel size for LDK to accept (reserve balance)
const ldkMinimum = Math.round(clientBalance * 0.025);
// Channel size must be at least minChannelSize
const lspMinimum = Math.max(minChannelSize - clientBalance, 0);

return Math.max(ldkMinimum, lspMinimum);
};

const getMaxClientBalance = (
onchainBalance: number,
maxChannelSize: number,
): number => {
// Remote balance must be at least 2% of the channel size for LDK to accept (reserve balance)
const minRemoteBalance = Math.round(maxChannelSize * 0.02);
// Cap client balance to 80% to leave buffer for fees
const feeMaximum = Math.round(onchainBalance * 0.8);
const ldkMaximum = maxChannelSize - minRemoteBalance;

return Math.min(feeMaximum, ldkMaximum);
const getMaxClientBalance = (maxChannelSize: number): number => {
// Remote balance must be at least 2.5% of the channel size for LDK to accept (reserve balance)
const minRemoteBalance = Math.round(maxChannelSize * 0.025);
return maxChannelSize - minRemoteBalance;
};

/**
Expand All @@ -64,7 +59,6 @@ const getMaxClientBalance = (
*/
export const useTransfer = (clientBalance: number): TTransferValues => {
const blocktankInfo = useAppSelector(blocktankInfoSelector);
const onchainBalance = useAppSelector(onChainBalanceSelector);
const channelsSize = useAppSelector(blocktankChannelsSizeSelector);

const { minChannelSizeSat, maxChannelSizeSat } = blocktankInfo.options;
Expand All @@ -77,9 +71,9 @@ export const useTransfer = (clientBalance: number): TTransferValues => {
const maxChannelSize = Math.min(maxChannelSize1, maxChannelSize2);

const minLspBalance = getMinLspBalance(clientBalance, minChannelSizeSat);
const maxLspBalance = maxChannelSize - clientBalance;
const maxLspBalance = Math.max(maxChannelSize - clientBalance, 0);
const defaultLspBalance = getDefaultLspBalance(clientBalance, maxLspBalance);
const maxClientBalance = getMaxClientBalance(onchainBalance, maxChannelSize);
const maxClientBalance = getMaxClientBalance(maxChannelSize);

return {
defaultLspBalance,
Expand All @@ -88,3 +82,45 @@ export const useTransfer = (clientBalance: number): TTransferValues => {
maxClientBalance,
};
};

/**
* Returns limits and default values for channel orders with the LSP
* @param {number} lspBalance
* @param {number} clientBalance
* @returns {{ fee: number, loading: boolean, error: string | null }}
*/
export const useTransferFee = (
lspBalance: number,
clientBalance: number,
): { fee: number; loading: boolean; error: string | null } => {
const [{ fee, loading, error }, setState] = useState<{
fee: number;
loading: boolean;
error: string | null;
}>({
fee: 0,
loading: true,
error: null,
});

useEffect(() => {
const getFeeEstimation = async (): Promise<void> => {
setState((prevState) => ({ ...prevState, loading: true }));
try {
const result = await estimateOrderFee({ lspBalance, clientBalance });
if (result.isOk()) {
const { feeSat } = result.value;
setState({ fee: feeSat, loading: false, error: null });
} else {
setState({ fee: 0, loading: false, error: result.error.message });
}
} catch (err) {
setState({ fee: 0, loading: false, error: err });
}
};

getFeeEstimation();
}, [lspBalance, clientBalance]);

return { fee, loading, error };
};
8 changes: 1 addition & 7 deletions src/screens/Transfer/SpendingAdvanced.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,7 @@ const SpendingAdvanced = ({
return;
}

const result = await estimateOrderFee({
lspBalance,
options: {
clientBalanceSat: clientBalance,
turboChannel: false,
},
});
const result = await estimateOrderFee({ lspBalance, clientBalance });
if (result.isErr()) {
return;
}
Expand Down
80 changes: 49 additions & 31 deletions src/screens/Transfer/SpendingAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@ import Button from '../../components/buttons/Button';
import UnitButton from '../Wallets/UnitButton';
import TransferNumberPad from './TransferNumberPad';
import type { TransferScreenProps } from '../../navigation/types';
import { useTransfer } from '../../hooks/transfer';
import { useTransfer, useTransferFee } from '../../hooks/transfer';
import { useAppSelector } from '../../hooks/redux';
import { useBalance, useSwitchUnit } from '../../hooks/wallet';
import { convertToSats } from '../../utils/conversion';
import { showToast } from '../../utils/notifications';
import { getNumberPadText } from '../../utils/numberpad';
import { getDisplayValues } from '../../utils/displayValues';
import { getMaxSendAmount } from '../../utils/wallet/transactions';
import { transactionSelector } from '../../store/reselect/wallet';
import {
resetSendTransaction,
Expand All @@ -45,6 +44,7 @@ import {
conversionUnitSelector,
denominationSelector,
} from '../../store/reselect/settings';
import { onChainFeesSelector } from '../../store/reselect/fees';

const SpendingAmount = ({
navigation,
Expand All @@ -57,37 +57,49 @@ const SpendingAmount = ({
const nextUnit = useAppSelector(nextUnitSelector);
const conversionUnit = useAppSelector(conversionUnitSelector);
const denomination = useAppSelector(denominationSelector);
const fees = useAppSelector(onChainFeesSelector);

const [textFieldValue, setTextFieldValue] = useState('');
const [loading, setLoading] = useState(false);

const clientBalance = useMemo((): number => {
return convertToSats(textFieldValue, conversionUnit);
}, [textFieldValue, conversionUnit]);

const transferValues = useTransfer(clientBalance);
const { minLspBalance, defaultLspBalance, maxClientBalance } = transferValues;

// Calculate the maximum amount that can be transferred
const availableAmount = onchainBalance - transaction.fee;
const { defaultLspBalance: maxLspBalance } = useTransfer(availableAmount);
const { fee: maxLspFee } = useTransferFee(maxLspBalance, availableAmount);
const feeMaximum = Math.floor(availableAmount - maxLspFee);
const maximum = Math.min(maxClientBalance, feeMaximum);

useFocusEffect(
useCallback(() => {
const setupTransfer = async (): Promise<void> => {
// In case of the low fee market, we bump fee by 5 sats
// details: https://github.com/synonymdev/bitkit/issues/2139
const getSatsPerByte = (fee: number): number => {
const MIN_FEE = 10;
const BUMP_FEE = 5;
return fee <= MIN_FEE ? fee + BUMP_FEE : fee;
};

const satsPerByte = getSatsPerByte(fees.fast);

await resetSendTransaction();
await setupOnChainTransaction({ rbf: false });
await setupOnChainTransaction({ satsPerByte, rbf: false });
refreshBlocktankInfo().then();
};
setupTransfer();

// onMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []),
);

const clientBalance = useMemo((): number => {
return convertToSats(textFieldValue, conversionUnit);
}, [textFieldValue, conversionUnit]);

const transferValues = useTransfer(clientBalance);
const { defaultLspBalance, maxClientBalance } = transferValues;

const availableAmount = useMemo(() => {
const maxAmountResponse = getMaxSendAmount();
if (maxAmountResponse.isOk()) {
return maxAmountResponse.value.amount;
}
return 0;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transaction.outputs, transaction.satsPerByte]);

const onChangeUnit = (): void => {
const result = getNumberPadText(clientBalance, denomination, nextUnit);
setTextFieldValue(result);
Expand All @@ -96,37 +108,43 @@ const SpendingAmount = ({

const onQuarter = (): void => {
const quarter = Math.round(onchainBalance / 4);
const amount = Math.min(quarter, maxClientBalance);
const amount = Math.min(quarter, maximum);
const result = getNumberPadText(amount, denomination, unit);
setTextFieldValue(result);
};

const onMaxAmount = (): void => {
const result = getNumberPadText(maxClientBalance, denomination, unit);
const result = getNumberPadText(maximum, denomination, unit);
setTextFieldValue(result);
};

const onNumberPadError = (): void => {
const dv = getDisplayValues({ satoshis: maxClientBalance });
const dv = getDisplayValues({ satoshis: maximum });
let description = t('spending_amount.error_max.description', {
amount: dv.bitcoinFormatted,
});

if (maximum === 0) {
description = t('spending_amount.error_max.description_zero');
}

showToast({
type: 'warning',
title: t('spending_amount.error_max.title'),
description: t('spending_amount.error_max.description', {
amount: dv.bitcoinFormatted,
}),
description,
});
};

const onContinue = async (): Promise<void> => {
setLoading(true);

const lspBalance = defaultLspBalance;
const response = await startChannelPurchase({ clientBalance, lspBalance });
const lspBalance = Math.max(defaultLspBalance, minLspBalance);
const result = await startChannelPurchase({ clientBalance, lspBalance });

setLoading(false);

if (response.isErr()) {
const { message } = response.error;
if (result.isErr()) {
const { message } = result.error;
const nodeCapped = message.includes('channel size check');
const title = nodeCapped
? t('spending_amount.error_max.title')
Expand All @@ -143,7 +161,7 @@ const SpendingAmount = ({
return;
}

navigation.navigate('SpendingConfirm', { order: response.value });
navigation.navigate('SpendingConfirm', { order: result.value });
};

return (
Expand Down Expand Up @@ -222,7 +240,7 @@ const SpendingAmount = ({
<TransferNumberPad
style={styles.numberpad}
value={textFieldValue}
maxAmount={maxClientBalance}
maxAmount={maximum}
onChange={setTextFieldValue}
onError={onNumberPadError}
/>
Expand Down
6 changes: 4 additions & 2 deletions src/screens/Wallets/Receive/ReceiveAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const ReceiveAmount = ({
const switchUnit = useSwitchUnit();
const [minimumAmount, setMinimumAmount] = useState(0);

const { defaultLspBalance: lspBalance } = useTransfer(0);
const { defaultLspBalance: lspBalance, maxClientBalance } = useTransfer(0);

useFocusEffect(
useCallback(() => {
Expand Down Expand Up @@ -95,7 +95,9 @@ const ReceiveAmount = ({
};

const continueDisabled =
minimumAmount === 0 || invoice.amount < minimumAmount;
minimumAmount === 0 ||
invoice.amount < minimumAmount ||
invoice.amount > maxClientBalance;

return (
<GradientView style={styles.container}>
Expand Down
7 changes: 5 additions & 2 deletions src/screens/Wallets/Receive/ReceiveConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ const ReceiveConnect = ({
useEffect(() => {
const getFeeEstimation = async (): Promise<void> => {
setIsLoading(true);
const feeResult = await estimateOrderFee({ lspBalance });
const feeResult = await estimateOrderFee({
lspBalance,
clientBalance: amount,
});
if (feeResult.isOk()) {
const fees = feeResult.value;
setFeeEstimate(fees);
Expand All @@ -163,7 +166,7 @@ const ReceiveConnect = ({
};

getFeeEstimation();
}, [t, lspBalance]);
}, [t, lspBalance, amount]);

return (
<GradientView style={styles.container}>
Expand Down
2 changes: 1 addition & 1 deletion src/screens/Wallets/Receive/ReceiveDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const ReceiveDetails = ({
};

getFeeEstimation();
}, [lspBalance, t]);
}, [lspBalance]);

useEffect(() => {
if (invoice.tags.length > 0) {
Expand Down
6 changes: 2 additions & 4 deletions src/store/reselect/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,8 @@ export const transactionFeeSelector = createSelector(
[walletState],
(wallet) => {
const { selectedWallet, selectedNetwork } = wallet;
return (
wallet.wallets[selectedWallet]?.transaction[selectedNetwork].fee ||
defaultSendTransaction.fee
);
const { transaction } = wallet.wallets[selectedWallet];
return transaction[selectedNetwork].fee;
},
);

Expand Down
Loading
Loading