Skip to content

Commit

Permalink
fix(transfer): update liquidity policy
Browse files Browse the repository at this point in the history
  • Loading branch information
pwltr committed Dec 9, 2024
1 parent b5d6bb3 commit e0a32b5
Show file tree
Hide file tree
Showing 16 changed files with 330 additions and 381 deletions.
85 changes: 1 addition & 84 deletions __tests__/reselect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import '../src/utils/i18n';
import store, { RootState } from '../src/store';
import { dispatch } from '../src/store/helpers';
import { updateWallet } from '../src/store/slices/wallet';
import {
TBalance,
balanceSelector,
transferLimitsSelector,
} from '../src/store/reselect/aggregations';
import { TBalance, balanceSelector } from '../src/store/reselect/aggregations';
import {
EChannelClosureReason,
EChannelStatus,
Expand Down Expand Up @@ -121,83 +117,4 @@ describe('Reselect', () => {
assert.deepEqual(balanceSelector(state), balance);
});
});

describe('transferLimitsSelector', () => {
it('should calculate limits without LN channels', () => {
// max value is limited by maxChannelSize / 2
const s1 = cloneDeep(s);
s1.wallet.wallets.wallet0.balance.bitcoinRegtest = 1000;
s1.blocktank.info.options = {
...s1.blocktank.info.options,
minChannelSizeSat: 10,
maxChannelSizeSat: 200,
maxClientBalanceSat: 100,
};

const received1 = transferLimitsSelector(s1);
const expected1 = {
minChannelSize: 11,
maxChannelSize: 190,
maxClientBalance: 95,
};

expect(received1).toMatchObject(expected1);

// max value is limited by onchain balance
const s2 = cloneDeep(s);
s2.wallet.wallets.wallet0.balance.bitcoinRegtest = 50;
s2.blocktank.info.options = {
...s2.blocktank.info.options,
minChannelSizeSat: 10,
maxChannelSizeSat: 200,
maxClientBalanceSat: 100,
};

const received2 = transferLimitsSelector(s2);
const expected2 = {
minChannelSize: 11,
maxChannelSize: 190,
maxClientBalance: 40,
};

expect(received2).toMatchObject(expected2);
});

it('should calculate limits with existing LN channels', () => {
const btNodeId =
'03b9a456fb45d5ac98c02040d39aec77fa3eeb41fd22cf40b862b393bcfc43473a';
// max value is limited by leftover node capacity
const s1 = cloneDeep(s);
s1.wallet.wallets.wallet0.balance.bitcoinRegtest = 1000;
s1.blocktank.info.nodes = [
{ alias: 'node1', pubkey: btNodeId, connectionStrings: [] },
];
s1.blocktank.info.options = {
...s1.blocktank.info.options,
minChannelSizeSat: 10,
maxChannelSizeSat: 200,
};

const channel1 = {
channel_id: 'channel1',
status: EChannelStatus.open,
is_channel_ready: true,
outbound_capacity_sat: 1,
balance_sat: 2,
channel_value_satoshis: 100,
counterparty_node_id: btNodeId,
} as TChannel;
const lnWallet = s1.lightning.nodes.wallet0;
lnWallet.channels.bitcoinRegtest = { channel1 };

const received1 = transferLimitsSelector(s1);
const expected1 = {
minChannelSize: 11,
maxChannelSize: 90,
maxClientBalance: 45,
};

expect(received1).toMatchObject(expected1);
});
});
});
19 changes: 9 additions & 10 deletions e2e/channels.e2e.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import createLnRpc from '@radar/lnrpc';
import BitcoinJsonRpc from 'bitcoin-json-rpc';
import jestExpect from 'expect';

import initWaitForElectrumToSync from '../__tests__/utils/wait-for-electrum';
import {
Expand All @@ -10,7 +9,6 @@ import {
completeOnboarding,
electrumHost,
electrumPort,
isButtonEnabled,
launchAndWait,
markComplete,
sleep,
Expand Down Expand Up @@ -90,17 +88,18 @@ d('Transfer', () => {
await element(by.id('FundTransfer')).tap();
await element(by.id('SpendingIntro-button')).tap();

// default amount is 0
const button = element(by.id('SpendingAmountContinue'));
const buttonEnabled = await isButtonEnabled(button);
jestExpect(buttonEnabled).toBe(false);
// can continue with default client balance (0)
await element(by.id('SpendingAmountContinue')).tap();
await element(by.id('SpendingConfirmAdvanced')).tap();
await element(by.id('SpendingAdvancedMin')).tap();
await element(by.id('NavigationBack')).tap();

// can continue with max amount
// can continue with max client balance
await element(by.id('SpendingAmountMax')).tap();
await element(by.id('SpendingAmountContinue')).tap();
await element(by.id('NavigationBack')).tap();

// can continue with 25% amount
// can continue with 25% client balance
await element(by.id('SpendingAmountQuarter')).tap();
await expect(element(by.text('250 000'))).toBeVisible();
await element(by.id('SpendingAmountContinue')).tap();
Expand All @@ -109,7 +108,7 @@ d('Transfer', () => {
await element(by.id('NavigationBack')).tap();
await element(by.id('SpendingIntro-button')).tap();

// can change amount
// can change client balance
await element(by.id('N2').withAncestor(by.id('SpendingAmount'))).tap();
await element(by.id('N0').withAncestor(by.id('SpendingAmount'))).multiTap(
5,
Expand Down Expand Up @@ -141,7 +140,7 @@ d('Transfer', () => {
// Receiving Capacity
// can continue with min amount
await element(by.id('SpendingAdvancedMin')).tap();
await expect(element(by.text('105 000'))).toBeVisible();
await expect(element(by.text('2 000'))).toBeVisible();
await element(by.id('SpendingAdvancedContinue')).tap();
await element(by.id('SpendingConfirmDefault')).tap();
await element(by.id('SpendingConfirmAdvanced')).tap();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@react-navigation/native-stack": "6.10.1",
"@reduxjs/toolkit": "2.2.6",
"@shopify/react-native-skia": "1.3.11",
"@synonymdev/blocktank-lsp-http-client": "2.0.0",
"@synonymdev/blocktank-lsp-http-client": "2.2.0",
"@synonymdev/feeds": "3.0.0",
"@synonymdev/react-native-ldk": "0.0.154",
"@synonymdev/react-native-lnurl": "0.0.10",
Expand Down
6 changes: 5 additions & 1 deletion src/components/ActivityIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import Animated, {
withTiming,
} from 'react-native-reanimated';

export const ActivityIndicator = ({ size }: { size: number }): ReactElement => {
export const ActivityIndicator = ({
size = 32,
}: {
size?: number;
}): ReactElement => {
const strokeWidth = size / 12;
const radius = (size - strokeWidth) / 2;
const canvasSize = size + 30;
Expand Down
90 changes: 90 additions & 0 deletions src/hooks/transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useAppSelector } from './redux';
import { onChainBalanceSelector } from '../store/reselect/wallet';
import { blocktankInfoSelector } from '../store/reselect/blocktank';
import { blocktankChannelsSizeSelector } from '../store/reselect/lightning';
import { fiatToBitcoinUnit } from '../utils/conversion';

type TTransferValues = {
maxClientBalance: number;
defaultLspBalance: number;
minLspBalance: number;
maxLspBalance: number;
};

const getDefaultLspBalance = (
clientBalance: number,
maxLspBalance: number,
): number => {
const threshold1 = fiatToBitcoinUnit({ amount: 225, currency: 'EUR' });
const threshold2 = fiatToBitcoinUnit({ amount: 495, currency: 'EUR' });
const defaultLspBalance = fiatToBitcoinUnit({ amount: 450, currency: 'EUR' });

let lspBalance = defaultLspBalance - clientBalance;

if (clientBalance > threshold1) {
lspBalance = clientBalance;
}

if (clientBalance > threshold2) {
lspBalance = maxLspBalance;
}

return Math.min(lspBalance, maxLspBalance);
};

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);
// 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);
};

/**
* Returns limits and default values for channel orders with the LSP
* @param {number} clientBalance
* @returns {TTransferValues}
*/
export const useTransfer = (clientBalance: number): TTransferValues => {
const blocktankInfo = useAppSelector(blocktankInfoSelector);
const onchainBalance = useAppSelector(onChainBalanceSelector);
const channelsSize = useAppSelector(blocktankChannelsSizeSelector);

const { minChannelSizeSat, maxChannelSizeSat } = blocktankInfo.options;

// Because LSP limits constantly change depending on network fees
// add a 2% buffer to avoid fluctuations while making the order
const maxChannelSize1 = Math.round(maxChannelSizeSat * 0.98);
// The maximum channel size the user can open including existing channels
const maxChannelSize2 = Math.max(0, maxChannelSize1 - channelsSize);
const maxChannelSize = Math.min(maxChannelSize1, maxChannelSize2);

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

return {
defaultLspBalance,
minLspBalance,
maxLspBalance,
maxClientBalance,
};
};
15 changes: 6 additions & 9 deletions src/screens/Transfer/SpendingAdvanced.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import Button from '../../components/buttons/Button';
import TransferNumberPad from './TransferNumberPad';
import { useAppSelector } from '../../hooks/redux';
import { useSwitchUnit } from '../../hooks/wallet';
import { useTransfer } from '../../hooks/transfer';
import { convertToSats } from '../../utils/conversion';
import { showToast } from '../../utils/notifications';
import { estimateOrderFee } from '../../utils/blocktank';
import { getNumberPadText } from '../../utils/numberpad';
import type { TransferScreenProps } from '../../navigation/types';
import { transferLimitsSelector } from '../../store/reselect/aggregations';
import { startChannelPurchase } from '../../store/utils/blocktank';
import {
nextUnitSelector,
Expand All @@ -40,18 +40,14 @@ const SpendingAdvanced = ({
const nextUnit = useAppSelector(nextUnitSelector);
const conversionUnit = useAppSelector(conversionUnitSelector);
const denomination = useAppSelector(denominationSelector);
const limits = useAppSelector(transferLimitsSelector);
const transferValues = useTransfer(order.clientBalanceSat);
const { defaultLspBalance, minLspBalance, maxLspBalance } = transferValues;

const [textFieldValue, setTextFieldValue] = useState('');
const [loading, setLoading] = useState(false);
const [feeEstimate, setFeeEstimate] = useState<{ [key: string]: number }>({});

const clientBalance = order.clientBalanceSat;
const { minChannelSize, maxChannelSize } = limits;
// LSP balance should be at least half of the channel size
// TODO: get exact requirements from LSP
const minLspBalance = Math.max(minChannelSize, clientBalance);
const maxLspBalance = Math.round(maxChannelSize - clientBalance);

const lspBalance = useMemo((): number => {
return convertToSats(textFieldValue, conversionUnit);
Expand Down Expand Up @@ -80,9 +76,11 @@ const SpendingAdvanced = ({
return;
}

const fee = result.value.feeSat;

setFeeEstimate((value) => ({
...value,
[`${clientBalance}-${lspBalance}`]: result.value,
[`${clientBalance}-${lspBalance}`]: fee,
}));
};

Expand All @@ -98,7 +96,6 @@ const SpendingAdvanced = ({
};

const onDefault = (): void => {
const defaultLspBalance = Math.round(maxChannelSize / 2);
const result = getNumberPadText(defaultLspBalance, denomination, unit);
setTextFieldValue(result);
};
Expand Down
11 changes: 5 additions & 6 deletions src/screens/Transfer/SpendingAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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 { useAppSelector } from '../../hooks/redux';
import { useBalance, useSwitchUnit } from '../../hooks/wallet';
import { convertToSats } from '../../utils/conversion';
Expand All @@ -30,7 +31,6 @@ import { getNumberPadText } from '../../utils/numberpad';
import { getDisplayValues } from '../../utils/displayValues';
import { getMaxSendAmount } from '../../utils/wallet/transactions';
import { transactionSelector } from '../../store/reselect/wallet';
import { transferLimitsSelector } from '../../store/reselect/aggregations';
import {
resetSendTransaction,
setupOnChainTransaction,
Expand All @@ -57,7 +57,6 @@ const SpendingAmount = ({
const nextUnit = useAppSelector(nextUnitSelector);
const conversionUnit = useAppSelector(conversionUnitSelector);
const denomination = useAppSelector(denominationSelector);
const limits = useAppSelector(transferLimitsSelector);

const [textFieldValue, setTextFieldValue] = useState('');
const [loading, setLoading] = useState(false);
Expand All @@ -73,12 +72,13 @@ const SpendingAmount = ({
}, []),
);

const { maxChannelSize, maxClientBalance } = limits;

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()) {
Expand Down Expand Up @@ -120,7 +120,7 @@ const SpendingAmount = ({
const onContinue = async (): Promise<void> => {
setLoading(true);

const lspBalance = Math.round(maxChannelSize / 2);
const lspBalance = defaultLspBalance;
const response = await startChannelPurchase({ clientBalance, lspBalance });

setLoading(false);
Expand Down Expand Up @@ -234,7 +234,6 @@ const SpendingAmount = ({
text={t('continue')}
size="large"
loading={loading}
disabled={!clientBalance}
testID="SpendingAmountContinue"
onPress={onContinue}
/>
Expand Down
Loading

0 comments on commit e0a32b5

Please sign in to comment.