Skip to content

Commit 649ebc9

Browse files
committed
pool+orders: add channel duration option to order form
1 parent 31be43b commit 649ebc9

File tree

7 files changed

+101
-26
lines changed

7 files changed

+101
-26
lines changed

app/src/__tests__/components/pool/OrderFormSection.spec.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('OrderFormSection', () => {
1313
store = createStore();
1414
await store.accountStore.fetchAccounts();
1515
await store.orderStore.fetchOrders();
16+
await store.batchStore.fetchLeaseDurations();
1617
});
1718

1819
const render = () => {
@@ -101,6 +102,31 @@ describe('OrderFormSection', () => {
101102
expect(ask!.details.maxBatchFeeRateSatPerKw).toBe(253);
102103
});
103104

105+
it('should submit an order with a different lease duration', async () => {
106+
const { getByText, changeInput, changeSelect } = render();
107+
108+
changeInput('Desired Inbound Liquidity', '1000000');
109+
changeInput('Bid Premium', '10000');
110+
changeInput('Minimum Channel Size', '100000');
111+
changeInput('Max Batch Fee Rate', '1');
112+
await changeSelect('Channel Duration', '4032');
113+
await changeSelect('Min Node Tier', 'T0 - All Nodes');
114+
115+
let bid: Required<POOL.Bid.AsObject>;
116+
// capture the rate that is sent to the API
117+
injectIntoGrpcUnary((_, props) => {
118+
bid = (props.request.toObject() as any).bid;
119+
});
120+
121+
fireEvent.click(getByText('Place Bid Order'));
122+
expect(bid!.details.amt).toBe(1000000);
123+
expect(bid!.details.rateFixed).toBe(2480);
124+
expect(bid!.details.minUnitsMatch).toBe(1);
125+
expect(bid!.leaseDurationBlocks).toBe(4032);
126+
expect(bid!.minNodeTier).toBe(1);
127+
expect(bid!.details.maxBatchFeeRateSatPerKw).toBe(253);
128+
});
129+
104130
it('should reset the form after placing an order', async () => {
105131
const { getByText, getByLabelText, changeInput } = render();
106132
changeInput('Desired Inbound Liquidity', '1000000');
@@ -202,10 +228,10 @@ describe('OrderFormSection', () => {
202228
});
203229

204230
it('should display the channel duration', () => {
205-
const { getByText } = render();
206-
expect(getByText('Channel Duration')).toBeInTheDocument();
231+
const { getByText, getAllByText } = render();
232+
expect(getAllByText('Channel Duration')).toHaveLength(2);
207233
expect(getByText('2016 blocks')).toBeInTheDocument();
208-
expect(getByText('(~2 wks)')).toBeInTheDocument();
234+
expect(getByText('(~2 weeks)')).toBeInTheDocument();
209235
});
210236

211237
it('should calculate the per block rate', () => {

app/src/api/pool.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,12 @@ export const FEE_RATE_TOTAL_PARTS = 1e9;
1515
// The amount of satoshis in one unit
1616
export const ONE_UNIT = 100000;
1717

18-
// The duration of each order. This value is temporarily constant in the initial
19-
// release of Pool
20-
export const DURATION = 2016;
21-
2218
// The minimum batch fee rate in sats/kw
2319
export const MIN_FEE_RATE_KW = 253;
2420

2521
// The latest order version. This should be updated along with pool CLI
26-
export const ORDER_VERSION = 1;
22+
// see: https://github.com/lightninglabs/pool/blob/master/order/interface.go#L35
23+
export const ORDER_VERSION = 2;
2724

2825
/** the names and argument types for the subscription events */
2926
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -303,7 +300,7 @@ class PoolApi extends BaseApi<PoolEvents> {
303300
* @param amount the amount of the order
304301
* @param premium the premium being paid
305302
*/
306-
calcFixedRate(amount: number, premium: number) {
303+
calcFixedRate(amount: number, premium: number, duration: number) {
307304
const ratePct = (premium * 100) / amount;
308305
// rate = % / 100
309306
// rate = rateFixed / totalParts
@@ -312,15 +309,15 @@ class PoolApi extends BaseApi<PoolEvents> {
312309
const rateFixedFloat = interestRate * FEE_RATE_TOTAL_PARTS;
313310
// We then take this rate fixed, and divide it by the number of blocks
314311
// as the user wants this rate to be the final lump sum they pay.
315-
return Math.floor(rateFixedFloat / DURATION);
312+
return Math.floor(rateFixedFloat / duration);
316313
}
317314

318315
/**
319316
* Calculates the percentage interest rate for a given fixed rate
320317
* @param fixedRate the per block fixed rate
321318
*/
322-
calcPctRate(fixedRate: number) {
323-
return (fixedRate * DURATION) / FEE_RATE_TOTAL_PARTS;
319+
calcPctRate(fixedRate: number, duration: number) {
320+
return (fixedRate * duration) / FEE_RATE_TOTAL_PARTS;
324321
}
325322
}
326323

app/src/components/pool/OrderFormSection.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { observer } from 'mobx-react-lite';
3+
import { LeaseDuration } from 'types/state';
34
import { usePrefixedTranslation } from 'hooks';
45
import { Unit, Units } from 'util/constants';
56
import { useStore } from 'store';
@@ -13,6 +14,7 @@ import {
1314
Small,
1415
SummaryItem,
1516
} from 'components/base';
17+
import BlockTime from 'components/common/BlockTime';
1618
import FormField from 'components/common/FormField';
1719
import FormInputNumber from 'components/common/FormInputNumber';
1820
import FormSelect from 'components/common/FormSelect';
@@ -40,7 +42,7 @@ const Styled = {
4042
`,
4143
Options: styled.div<{ visible: boolean }>`
4244
overflow: hidden;
43-
max-height: ${props => (props.visible ? '300px' : '0')};
45+
max-height: ${props => (props.visible ? '360px' : '0')};
4446
transition: max-height 0.3s linear;
4547
`,
4648
OptionsButton: styled(Button)`
@@ -127,6 +129,14 @@ const OrderFormSection: React.FC = () => {
127129
/>
128130
</FormField>
129131
<Options visible={orderFormView.addlOptionsVisible}>
132+
<FormField label={l('durationLabel')}>
133+
<FormSelect
134+
label={l('durationLabel')}
135+
value={orderFormView.duration.toString()}
136+
onChange={v => orderFormView.setDuration(parseInt(v) as LeaseDuration)}
137+
options={orderFormView.durationOptions}
138+
/>
139+
</FormField>
130140
<FormField label={l('minChanSizeLabel')} error={orderFormView.minChanSizeError}>
131141
<FormInputNumber
132142
label={l('minChanSizeLabel')}
@@ -168,9 +178,11 @@ const OrderFormSection: React.FC = () => {
168178
<SummaryItem>
169179
<span>{l('durationLabel')}</span>
170180
<span className="text-right">
171-
2016 blocks
181+
{orderFormView.derivedDuration} blocks
172182
<br />
173-
<Small>(~{l('durationWeeks')})</Small>
183+
<Small>
184+
(<BlockTime blocks={orderFormView.derivedDuration} />)
185+
</Small>
174186
</span>
175187
</SummaryItem>
176188
<SummaryItem>

app/src/i18n/locales/en-US.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,15 +196,14 @@
196196
"cmps.pool.OrderFormSection.premiumLabelAsk": "Ask Premium",
197197
"cmps.pool.OrderFormSection.premiumPlaceholder": "5,000",
198198
"cmps.pool.OrderFormSection.premiumSuggested": "Suggested",
199+
"cmps.pool.OrderFormSection.durationLabel": "Channel Duration",
199200
"cmps.pool.OrderFormSection.minChanSizeLabel": "Minimum Channel Size",
200201
"cmps.pool.OrderFormSection.minChanSizePlaceholder": "100,000",
201202
"cmps.pool.OrderFormSection.viewOptions": "View Additional Options",
202203
"cmps.pool.OrderFormSection.hideOptions": "Hide Additional Options",
203204
"cmps.pool.OrderFormSection.feeLabel": "Max Batch Fee Rate",
204205
"cmps.pool.OrderFormSection.feePlaceholder": "100",
205206
"cmps.pool.OrderFormSection.tierLabel": "Min Node Tier",
206-
"cmps.pool.OrderFormSection.durationLabel": "Channel Duration",
207-
"cmps.pool.OrderFormSection.durationWeeks": "2 wks",
208207
"cmps.pool.OrderFormSection.fixedRateLabel": "Per Block Fixed Rate",
209208
"cmps.pool.OrderFormSection.interestLabel": "Interest Rate",
210209
"cmps.pool.OrderFormSection.aprLabel": "Annual Rate (APR)",
@@ -288,6 +287,7 @@
288287
"stores.orderFormView.errorMultiple": "must be a multiple of 100,000",
289288
"stores.orderFormView.premiumLowError": "per block fixed rate is too small",
290289
"stores.orderFormView.errorLiquidity": "must be less than liquidity amount",
290+
"stores.orderFormView.inView": "Currently Viewing",
291291
"stores.orderFormView.feeRateErrorMin": "minimum {{min}} sats/vByte",
292292
"stores.orderFormView.placeOrderLabel": "Place {{action}} Order",
293293
"stores.settingsStore.required": "a valid url is required",

app/src/store/models/batch.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ export default class Batch {
100100

101101
/** the total amount of sats earned in this batch */
102102
get earnedSats() {
103-
const pctRate = this._store.api.pool.calcPctRate(this.clearingPriceRate);
103+
const pctRate = this._store.api.pool.calcPctRate(
104+
this.clearingPriceRate,
105+
this.leaseDuration,
106+
);
104107
return this.volume.mul(pctRate);
105108
}
106109

@@ -141,7 +144,10 @@ export default class Batch {
141144

142145
/** the batch clearing rate expressed as basis points */
143146
get basisPoints() {
144-
const pct = this._store.api.pool.calcPctRate(this.clearingPriceRate);
147+
const pct = this._store.api.pool.calcPctRate(
148+
this.clearingPriceRate,
149+
this.leaseDuration,
150+
);
145151
return Math.round(pct * 100 * 100);
146152
}
147153

app/src/store/models/order.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default class Order {
5454

5555
/** the order fixed rate expressed as basis points */
5656
get basisPoints() {
57-
const pct = this._store.api.pool.calcPctRate(this.rateFixed);
57+
const pct = this._store.api.pool.calcPctRate(this.rateFixed, this.duration);
5858
return Math.round(pct * 100 * 100);
5959
}
6060

app/src/store/views/orderFormView.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { makeAutoObservable, runInAction } from 'mobx';
1+
import { keys, makeAutoObservable, runInAction } from 'mobx';
22
import { NodeTier } from 'types/generated/auctioneer_pb';
3+
import { LeaseDuration } from 'types/state';
34
import { annualPercentRate, toBasisPoints, toPercent } from 'util/bigmath';
45
import { BLOCKS_PER_DAY } from 'util/constants';
56
import { prefixTranslation } from 'util/translate';
6-
import { DURATION, ONE_UNIT } from 'api/pool';
7+
import { ONE_UNIT } from 'api/pool';
78
import { Store } from 'store';
89
import { NODE_TIERS, OrderType, Tier } from 'store/models/order';
910

@@ -19,6 +20,7 @@ export default class OrderFormView {
1920
orderType: OrderType = OrderType.Bid;
2021
amount = 0;
2122
premium = 0;
23+
duration = 0;
2224
minChanSize = DEFAULT_MIN_CHAN_SIZE;
2325
maxBatchFeeRate = DEFAULT_MAX_BATCH_FEE;
2426
minNodeTier: Tier = NodeTier.TIER_DEFAULT;
@@ -78,6 +80,26 @@ export default class OrderFormView {
7880
return '';
7981
}
8082

83+
/** the available options for the lease duration field */
84+
get durationOptions() {
85+
// add a default option with a value of zero to signify that the duration
86+
// currently being displayed should be used
87+
const current = {
88+
label: `${l('inView')} (${this._store.batchStore.selectedLeaseDuration})`,
89+
value: '0',
90+
};
91+
const durations = keys(this._store.batchStore.leaseDurations).map(duration => ({
92+
label: `${duration}`,
93+
value: `${duration}`,
94+
}));
95+
return [current, ...durations];
96+
}
97+
98+
/** the chosen duration or the value selected in the batch store */
99+
get derivedDuration() {
100+
return this.duration || this._store.batchStore.selectedLeaseDuration;
101+
}
102+
81103
/** the available options for the minNodeTier field */
82104
get nodeTierOptions() {
83105
return Object.entries(NODE_TIERS).map(([value, label]) => ({ label, value }));
@@ -87,7 +109,11 @@ export default class OrderFormView {
87109
get perBlockFixedRate() {
88110
if ([this.amount, this.premium].includes(0)) return 0;
89111

90-
return this._store.api.pool.calcFixedRate(this.amount, this.premium);
112+
return this._store.api.pool.calcFixedRate(
113+
this.amount,
114+
this.premium,
115+
this.derivedDuration,
116+
);
91117
}
92118

93119
/** the premium interest of the amount in basis points */
@@ -99,7 +125,7 @@ export default class OrderFormView {
99125
/** the APR given the amount and premium */
100126
get apr() {
101127
if ([this.amount, this.premium].includes(0)) return 0;
102-
const termInDays = DURATION / BLOCKS_PER_DAY;
128+
const termInDays = this.derivedDuration / BLOCKS_PER_DAY;
103129
const apr = annualPercentRate(this.amount, this.premium, termInDays);
104130
return toPercent(apr);
105131
}
@@ -132,6 +158,10 @@ export default class OrderFormView {
132158
this.premium = premium;
133159
}
134160

161+
setDuration(duration: LeaseDuration) {
162+
this.duration = duration;
163+
}
164+
135165
setMinChanSize(minChanSize: number) {
136166
this.minChanSize = minChanSize;
137167
}
@@ -151,7 +181,10 @@ export default class OrderFormView {
151181
if (!prevBatch) throw new Error('Previous batch not found');
152182
const prevFixedRate = prevBatch.clearingPriceRate;
153183
// get the percentage rate of the previous batch and apply to the current amount
154-
const prevPctRate = this._store.api.pool.calcPctRate(prevFixedRate);
184+
const prevPctRate = this._store.api.pool.calcPctRate(
185+
prevFixedRate,
186+
this.derivedDuration,
187+
);
155188
const suggested = this.amount * prevPctRate;
156189
// round to the nearest 10 to offset lose of precision in calculating percentages
157190
this.premium = Math.round(suggested / 10) * 10;
@@ -174,7 +207,7 @@ export default class OrderFormView {
174207
this.orderType,
175208
this.amount,
176209
this.perBlockFixedRate,
177-
DURATION,
210+
this.derivedDuration,
178211
minUnitsMatch,
179212
satsPerKWeight,
180213
this.minNodeTier,
@@ -183,6 +216,7 @@ export default class OrderFormView {
183216
if (nonce) {
184217
this.amount = 0;
185218
this.premium = 0;
219+
this.duration = 0;
186220
// persist the additional options so they can be used for future orders
187221
this._store.settingsStore.setOrderSettings(
188222
this.minChanSize,

0 commit comments

Comments
 (0)