Skip to content

Commit 1b395e8

Browse files
committed
fix(transfer): update liquidity policy
1 parent b5d6bb3 commit 1b395e8

File tree

16 files changed

+322
-372
lines changed

16 files changed

+322
-372
lines changed

__tests__/reselect.ts

Lines changed: 1 addition & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ import '../src/utils/i18n';
55
import store, { RootState } from '../src/store';
66
import { dispatch } from '../src/store/helpers';
77
import { updateWallet } from '../src/store/slices/wallet';
8-
import {
9-
TBalance,
10-
balanceSelector,
11-
transferLimitsSelector,
12-
} from '../src/store/reselect/aggregations';
8+
import { TBalance, balanceSelector } from '../src/store/reselect/aggregations';
139
import {
1410
EChannelClosureReason,
1511
EChannelStatus,
@@ -121,83 +117,4 @@ describe('Reselect', () => {
121117
assert.deepEqual(balanceSelector(state), balance);
122118
});
123119
});
124-
125-
describe('transferLimitsSelector', () => {
126-
it('should calculate limits without LN channels', () => {
127-
// max value is limited by maxChannelSize / 2
128-
const s1 = cloneDeep(s);
129-
s1.wallet.wallets.wallet0.balance.bitcoinRegtest = 1000;
130-
s1.blocktank.info.options = {
131-
...s1.blocktank.info.options,
132-
minChannelSizeSat: 10,
133-
maxChannelSizeSat: 200,
134-
maxClientBalanceSat: 100,
135-
};
136-
137-
const received1 = transferLimitsSelector(s1);
138-
const expected1 = {
139-
minChannelSize: 11,
140-
maxChannelSize: 190,
141-
maxClientBalance: 95,
142-
};
143-
144-
expect(received1).toMatchObject(expected1);
145-
146-
// max value is limited by onchain balance
147-
const s2 = cloneDeep(s);
148-
s2.wallet.wallets.wallet0.balance.bitcoinRegtest = 50;
149-
s2.blocktank.info.options = {
150-
...s2.blocktank.info.options,
151-
minChannelSizeSat: 10,
152-
maxChannelSizeSat: 200,
153-
maxClientBalanceSat: 100,
154-
};
155-
156-
const received2 = transferLimitsSelector(s2);
157-
const expected2 = {
158-
minChannelSize: 11,
159-
maxChannelSize: 190,
160-
maxClientBalance: 40,
161-
};
162-
163-
expect(received2).toMatchObject(expected2);
164-
});
165-
166-
it('should calculate limits with existing LN channels', () => {
167-
const btNodeId =
168-
'03b9a456fb45d5ac98c02040d39aec77fa3eeb41fd22cf40b862b393bcfc43473a';
169-
// max value is limited by leftover node capacity
170-
const s1 = cloneDeep(s);
171-
s1.wallet.wallets.wallet0.balance.bitcoinRegtest = 1000;
172-
s1.blocktank.info.nodes = [
173-
{ alias: 'node1', pubkey: btNodeId, connectionStrings: [] },
174-
];
175-
s1.blocktank.info.options = {
176-
...s1.blocktank.info.options,
177-
minChannelSizeSat: 10,
178-
maxChannelSizeSat: 200,
179-
};
180-
181-
const channel1 = {
182-
channel_id: 'channel1',
183-
status: EChannelStatus.open,
184-
is_channel_ready: true,
185-
outbound_capacity_sat: 1,
186-
balance_sat: 2,
187-
channel_value_satoshis: 100,
188-
counterparty_node_id: btNodeId,
189-
} as TChannel;
190-
const lnWallet = s1.lightning.nodes.wallet0;
191-
lnWallet.channels.bitcoinRegtest = { channel1 };
192-
193-
const received1 = transferLimitsSelector(s1);
194-
const expected1 = {
195-
minChannelSize: 11,
196-
maxChannelSize: 90,
197-
maxClientBalance: 45,
198-
};
199-
200-
expect(received1).toMatchObject(expected1);
201-
});
202-
});
203120
});

e2e/channels.e2e.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ d('Transfer', () => {
141141
// Receiving Capacity
142142
// can continue with min amount
143143
await element(by.id('SpendingAdvancedMin')).tap();
144-
await expect(element(by.text('105 000'))).toBeVisible();
144+
await expect(element(by.text('2 000'))).toBeVisible();
145145
await element(by.id('SpendingAdvancedContinue')).tap();
146146
await element(by.id('SpendingConfirmDefault')).tap();
147147
await element(by.id('SpendingConfirmAdvanced')).tap();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"@react-navigation/native-stack": "6.10.1",
4747
"@reduxjs/toolkit": "2.2.6",
4848
"@shopify/react-native-skia": "1.3.11",
49-
"@synonymdev/blocktank-lsp-http-client": "2.0.0",
49+
"@synonymdev/blocktank-lsp-http-client": "2.2.0",
5050
"@synonymdev/feeds": "3.0.0",
5151
"@synonymdev/react-native-ldk": "0.0.154",
5252
"@synonymdev/react-native-lnurl": "0.0.10",

src/components/ActivityIndicator.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import Animated, {
1616
withTiming,
1717
} from 'react-native-reanimated';
1818

19-
export const ActivityIndicator = ({ size }: { size: number }): ReactElement => {
19+
export const ActivityIndicator = ({
20+
size = 32,
21+
}: {
22+
size?: number;
23+
}): ReactElement => {
2024
const strokeWidth = size / 12;
2125
const radius = (size - strokeWidth) / 2;
2226
const canvasSize = size + 30;

src/hooks/transfer.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useAppSelector } from './redux';
2+
import { onChainBalanceSelector } from '../store/reselect/wallet';
3+
import { blocktankInfoSelector } from '../store/reselect/blocktank';
4+
import { blocktankChannelsSizeSelector } from '../store/reselect/lightning';
5+
import { fiatToBitcoinUnit } from '../utils/conversion';
6+
7+
type TTransferValues = {
8+
maxClientBalance: number;
9+
defaultLspBalance: number;
10+
minLspBalance: number;
11+
maxLspBalance: number;
12+
};
13+
14+
const getDefaultLspBalance = (
15+
clientBalance: number,
16+
maxLspBalance: number,
17+
): number => {
18+
const threshold1 = fiatToBitcoinUnit({ amount: 225, currency: 'EUR' });
19+
const threshold2 = fiatToBitcoinUnit({ amount: 495, currency: 'EUR' });
20+
const defaultLspBalance = fiatToBitcoinUnit({ amount: 450, currency: 'EUR' });
21+
22+
let lspBalance = defaultLspBalance - clientBalance;
23+
24+
if (clientBalance > threshold1) {
25+
lspBalance = clientBalance;
26+
}
27+
28+
if (clientBalance > threshold2) {
29+
lspBalance = maxLspBalance;
30+
}
31+
32+
return Math.min(lspBalance, maxLspBalance);
33+
};
34+
35+
const getMinLspBalance = (
36+
clientBalance: number,
37+
minChannelSize: number,
38+
): number => {
39+
// LSP balance must be at least 2% of the channel size for LDK to accept (reserve balance)
40+
const ldkMinimum = Math.round(clientBalance * 0.02);
41+
// Channel size must be at least minChannelSize
42+
const lspMinimum = Math.max(minChannelSize - clientBalance, 0);
43+
44+
return Math.max(ldkMinimum, lspMinimum);
45+
};
46+
47+
const getMaxClientBalance = (
48+
onchainBalance: number,
49+
maxChannelSize: number,
50+
): number => {
51+
// Remote balance must be at least 2% of the channel size for LDK to accept (reserve balance)
52+
const minRemoteBalance = Math.round(maxChannelSize * 0.02);
53+
// Cap client balance to 80% to leave buffer for fees
54+
const feeMaximum = Math.round(onchainBalance * 0.8);
55+
const ldkMaximum = maxChannelSize - minRemoteBalance;
56+
57+
return Math.min(feeMaximum, ldkMaximum);
58+
};
59+
60+
/**
61+
* Returns limits and default values for channel orders with the LSP
62+
* @param {number} clientBalance
63+
* @returns {TTransferValues}
64+
*/
65+
export const useTransfer = (clientBalance: number): TTransferValues => {
66+
const blocktankInfo = useAppSelector(blocktankInfoSelector);
67+
const onchainBalance = useAppSelector(onChainBalanceSelector);
68+
const channelsSize = useAppSelector(blocktankChannelsSizeSelector);
69+
70+
const { minChannelSizeSat, maxChannelSizeSat } = blocktankInfo.options;
71+
72+
// Because LSP limits constantly change depending on network fees
73+
// add a 2% buffer to avoid fluctuations while making the order
74+
const maxChannelSize1 = Math.round(maxChannelSizeSat * 0.98);
75+
// The maximum channel size the user can open including existing channels
76+
const maxChannelSize2 = Math.max(0, maxChannelSize1 - channelsSize);
77+
const maxChannelSize = Math.min(maxChannelSize1, maxChannelSize2);
78+
79+
const minLspBalance = getMinLspBalance(clientBalance, minChannelSizeSat);
80+
const maxLspBalance = maxChannelSize - clientBalance;
81+
const defaultLspBalance = getDefaultLspBalance(clientBalance, maxLspBalance);
82+
const maxClientBalance = getMaxClientBalance(onchainBalance, maxChannelSize);
83+
84+
return {
85+
defaultLspBalance,
86+
minLspBalance,
87+
maxLspBalance,
88+
maxClientBalance,
89+
};
90+
};

src/screens/Transfer/SpendingAdvanced.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import Button from '../../components/buttons/Button';
1515
import TransferNumberPad from './TransferNumberPad';
1616
import { useAppSelector } from '../../hooks/redux';
1717
import { useSwitchUnit } from '../../hooks/wallet';
18+
import { useTransfer } from '../../hooks/transfer';
1819
import { convertToSats } from '../../utils/conversion';
1920
import { showToast } from '../../utils/notifications';
2021
import { estimateOrderFee } from '../../utils/blocktank';
2122
import { getNumberPadText } from '../../utils/numberpad';
2223
import type { TransferScreenProps } from '../../navigation/types';
23-
import { transferLimitsSelector } from '../../store/reselect/aggregations';
2424
import { startChannelPurchase } from '../../store/utils/blocktank';
2525
import {
2626
nextUnitSelector,
@@ -40,18 +40,14 @@ const SpendingAdvanced = ({
4040
const nextUnit = useAppSelector(nextUnitSelector);
4141
const conversionUnit = useAppSelector(conversionUnitSelector);
4242
const denomination = useAppSelector(denominationSelector);
43-
const limits = useAppSelector(transferLimitsSelector);
43+
const transferValues = useTransfer(order.clientBalanceSat);
44+
const { defaultLspBalance, minLspBalance, maxLspBalance } = transferValues;
4445

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

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

5652
const lspBalance = useMemo((): number => {
5753
return convertToSats(textFieldValue, conversionUnit);
@@ -80,9 +76,11 @@ const SpendingAdvanced = ({
8076
return;
8177
}
8278

79+
const fee = result.value.feeSat;
80+
8381
setFeeEstimate((value) => ({
8482
...value,
85-
[`${clientBalance}-${lspBalance}`]: result.value,
83+
[`${clientBalance}-${lspBalance}`]: fee,
8684
}));
8785
};
8886

@@ -98,7 +96,6 @@ const SpendingAdvanced = ({
9896
};
9997

10098
const onDefault = (): void => {
101-
const defaultLspBalance = Math.round(maxChannelSize / 2);
10299
const result = getNumberPadText(defaultLspBalance, denomination, unit);
103100
setTextFieldValue(result);
104101
};

src/screens/Transfer/SpendingAmount.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Button from '../../components/buttons/Button';
2222
import UnitButton from '../Wallets/UnitButton';
2323
import TransferNumberPad from './TransferNumberPad';
2424
import type { TransferScreenProps } from '../../navigation/types';
25+
import { useTransfer } from '../../hooks/transfer';
2526
import { useAppSelector } from '../../hooks/redux';
2627
import { useBalance, useSwitchUnit } from '../../hooks/wallet';
2728
import { convertToSats } from '../../utils/conversion';
@@ -30,7 +31,6 @@ import { getNumberPadText } from '../../utils/numberpad';
3031
import { getDisplayValues } from '../../utils/displayValues';
3132
import { getMaxSendAmount } from '../../utils/wallet/transactions';
3233
import { transactionSelector } from '../../store/reselect/wallet';
33-
import { transferLimitsSelector } from '../../store/reselect/aggregations';
3434
import {
3535
resetSendTransaction,
3636
setupOnChainTransaction,
@@ -57,7 +57,6 @@ const SpendingAmount = ({
5757
const nextUnit = useAppSelector(nextUnitSelector);
5858
const conversionUnit = useAppSelector(conversionUnitSelector);
5959
const denomination = useAppSelector(denominationSelector);
60-
const limits = useAppSelector(transferLimitsSelector);
6160

6261
const [textFieldValue, setTextFieldValue] = useState('');
6362
const [loading, setLoading] = useState(false);
@@ -73,12 +72,13 @@ const SpendingAmount = ({
7372
}, []),
7473
);
7574

76-
const { maxChannelSize, maxClientBalance } = limits;
77-
7875
const clientBalance = useMemo((): number => {
7976
return convertToSats(textFieldValue, conversionUnit);
8077
}, [textFieldValue, conversionUnit]);
8178

79+
const transferValues = useTransfer(clientBalance);
80+
const { defaultLspBalance, maxClientBalance } = transferValues;
81+
8282
const availableAmount = useMemo(() => {
8383
const maxAmountResponse = getMaxSendAmount();
8484
if (maxAmountResponse.isOk()) {
@@ -120,7 +120,7 @@ const SpendingAmount = ({
120120
const onContinue = async (): Promise<void> => {
121121
setLoading(true);
122122

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

126126
setLoading(false);
@@ -234,7 +234,6 @@ const SpendingAmount = ({
234234
text={t('continue')}
235235
size="large"
236236
loading={loading}
237-
disabled={!clientBalance}
238237
testID="SpendingAmountContinue"
239238
onPress={onContinue}
240239
/>

src/screens/Transfer/SpendingConfirm.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import Money from '../../components/Money';
1313
import LightningChannel from '../../components/LightningChannel';
1414
import { sleep } from '../../utils/helpers';
1515
import { showToast } from '../../utils/notifications';
16+
import { useTransfer } from '../../hooks/transfer';
1617
import { useAppSelector } from '../../hooks/redux';
1718
import { TransferScreenProps } from '../../navigation/types';
1819
import { transactionFeeSelector } from '../../store/reselect/wallet';
19-
import { transferLimitsSelector } from '../../store/reselect/aggregations';
2020
import {
2121
confirmChannelPurchase,
2222
startChannelPurchase,
@@ -32,7 +32,7 @@ const SpendingConfirm = ({
3232
const { t } = useTranslation('lightning');
3333
const [loading, setLoading] = useState(false);
3434
const transactionFee = useAppSelector(transactionFeeSelector);
35-
const limits = useAppSelector(transferLimitsSelector);
35+
const { defaultLspBalance } = useTransfer(order.clientBalanceSat);
3636

3737
const clientBalance = order.clientBalanceSat;
3838
const lspBalance = order.lspBalanceSat;
@@ -51,9 +51,6 @@ const SpendingConfirm = ({
5151
};
5252

5353
const onDefault = async (): Promise<void> => {
54-
const { maxChannelSize } = limits;
55-
const defaultLspBalance = Math.round(maxChannelSize / 2);
56-
5754
const response = await startChannelPurchase({
5855
clientBalance,
5956
lspBalance: defaultLspBalance,

0 commit comments

Comments
 (0)