Skip to content

Commit 917a0e4

Browse files
committed
pool+order: display fee estimates in the order form
1 parent 923f941 commit 917a0e4

File tree

6 files changed

+171
-0
lines changed

6 files changed

+171
-0
lines changed

app/src/api/pool.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,27 @@ class PoolApi extends BaseApi<PoolEvents> {
161161
return res.toObject();
162162
}
163163

164+
/**
165+
* call the pool `QuoteOrder` RPC and return the response
166+
*/
167+
async quoteOrder(
168+
amount: number,
169+
rateFixed: number,
170+
duration: number,
171+
minUnitsMatch: number,
172+
feeRateSatPerKw: number,
173+
): Promise<POOL.QuoteOrderResponse.AsObject> {
174+
const req = new POOL.QuoteOrderRequest();
175+
req.setAmt(amount);
176+
req.setRateFixed(rateFixed);
177+
req.setLeaseDurationBlocks(duration);
178+
req.setMinUnitsMatch(minUnitsMatch);
179+
req.setMaxBatchFeeRateSatPerKw(feeRateSatPerKw);
180+
181+
const res = await this._grpc.request(Trader.QuoteOrder, req, this._meta);
182+
return res.toObject();
183+
}
184+
164185
/**
165186
* call the pool `SubmitOrder` RPC and return the response
166187
*/

app/src/components/pool/OrderFormSection.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { observer } from 'mobx-react-lite';
33
import { LeaseDuration } from 'types/state';
4+
import Big from 'big.js';
45
import { usePrefixedTranslation } from 'hooks';
56
import { Unit, Units } from 'util/constants';
67
import { useStore } from 'store';
@@ -18,8 +19,11 @@ import BlockTime from 'components/common/BlockTime';
1819
import FormField from 'components/common/FormField';
1920
import FormInputNumber from 'components/common/FormInputNumber';
2021
import FormSelect from 'components/common/FormSelect';
22+
import LoaderLines from 'components/common/LoaderLines';
2123
import StatusDot from 'components/common/StatusDot';
24+
import Tip from 'components/common/Tip';
2225
import Toggle from 'components/common/Toggle';
26+
import UnitCmp from 'components/common/Unit';
2327
import { styled } from 'components/theme';
2428

2529
const Styled = {
@@ -62,6 +66,11 @@ const Styled = {
6266
margin: 30px auto;
6367
text-align: center;
6468
`,
69+
LoaderLines: styled(LoaderLines)`
70+
.line {
71+
margin: 0 1px;
72+
}
73+
`,
6574
};
6675

6776
const OrderFormSection: React.FC = () => {
@@ -80,6 +89,7 @@ const OrderFormSection: React.FC = () => {
8089
OptionsStatus,
8190
Divider,
8291
Actions,
92+
LoaderLines,
8393
} = Styled;
8494
return (
8595
<Section>
@@ -177,6 +187,30 @@ const OrderFormSection: React.FC = () => {
177187
/>
178188
</OptionsButton>
179189
<Divider />
190+
<Tip overlay={l('executionFeeTip')} capitalize={false} placement="topRight">
191+
<SummaryItem>
192+
<span>{l('executionFeeLabel')}</span>
193+
<span>
194+
{orderFormView.quoteLoading ? (
195+
<LoaderLines />
196+
) : (
197+
<UnitCmp sats={Big(orderFormView.executionFee)} />
198+
)}
199+
</span>
200+
</SummaryItem>
201+
</Tip>
202+
<SummaryItem>
203+
<Tip overlay={l('chainFeeTip')} capitalize={false} placement="topRight">
204+
<span>{l('chainFeeLabel')}</span>
205+
</Tip>
206+
<span>
207+
{orderFormView.quoteLoading ? (
208+
<LoaderLines />
209+
) : (
210+
<UnitCmp sats={Big(orderFormView.worstChainFee)} />
211+
)}
212+
</span>
213+
</SummaryItem>
180214
<SummaryItem>
181215
<span>{l('durationLabel')}</span>
182216
<span className="text-right">

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@
232232
"cmps.pool.OrderFormSection.feeLabel": "Max Batch Fee Rate",
233233
"cmps.pool.OrderFormSection.feePlaceholder": "100",
234234
"cmps.pool.OrderFormSection.tierLabel": "Min Node Tier",
235+
"cmps.pool.OrderFormSection.executionFeeLabel": "Execution Fee",
236+
"cmps.pool.OrderFormSection.executionFeeTip": "Total fee paid to the auctioneer for executing this order",
237+
"cmps.pool.OrderFormSection.chainFeeLabel": "Worst Case Chain Fee",
238+
"cmps.pool.OrderFormSection.chainFeeTip": "Assumes chain fees for the footprint of (amount / min_chan_size) channel openings using the max_batch_fee_rate",
235239
"cmps.pool.OrderFormSection.fixedRateLabel": "Per Block Fixed Rate",
236240
"cmps.pool.OrderFormSection.interestLabel": "Interest Rate",
237241
"cmps.pool.OrderFormSection.aprLabel": "Annual Rate (APR)",

app/src/store/stores/orderStore.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,50 @@ export default class OrderStore {
165165
/** fetch leases at most once every 2 seconds when using this func */
166166
fetchLeasesThrottled = debounce(this.fetchLeases, 2000);
167167

168+
/**
169+
* Requests a fee quote for an order
170+
* @param amount the amount of the order
171+
* @param rateFixed the per block fixed rate
172+
* @param duration the number of blocks to keep the channel open for
173+
* @param minUnitsMatch the minimum number of units required to match this order
174+
* @param maxBatchFeeRate the maximum batch fee rate to allowed as sats per vByte
175+
*/
176+
async quoteOrder(
177+
amount: number,
178+
rateFixed: number,
179+
duration: number,
180+
minUnitsMatch: number,
181+
maxBatchFeeRate: number,
182+
): Promise<POOL.QuoteOrderResponse.AsObject> {
183+
try {
184+
this._store.log.info(`quoting an order for ${amount}sats`, {
185+
rateFixed,
186+
duration,
187+
minUnitsMatch,
188+
maxBatchFeeRate,
189+
});
190+
191+
const res = await this._store.api.pool.quoteOrder(
192+
amount,
193+
rateFixed,
194+
duration,
195+
minUnitsMatch,
196+
maxBatchFeeRate,
197+
);
198+
199+
return res;
200+
} catch (error) {
201+
this._store.appView.handleError(error, 'Unable to estimate order fees');
202+
return {
203+
ratePerBlock: rateFixed,
204+
ratePercent: 0,
205+
totalExecutionFeeSat: 0,
206+
totalPremiumSat: 0,
207+
worstCaseChainFeeSat: 0,
208+
};
209+
}
210+
}
211+
168212
/**
169213
* Submits an order to the market
170214
* @param type the type of order (bid or ask)

app/src/store/views/orderFormView.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
NodeTier,
55
} from 'types/generated/auctioneerrpc/auctioneer_pb';
66
import { LeaseDuration } from 'types/state';
7+
import debounce from 'lodash/debounce';
78
import { annualPercentRate, toBasisPoints, toPercent } from 'util/bigmath';
89
import { BLOCKS_PER_DAY } from 'util/constants';
910
import { prefixTranslation } from 'util/translate';
@@ -31,6 +32,11 @@ export default class OrderFormView {
3132
/** toggle to show or hide the additional options */
3233
addlOptionsVisible = false;
3334

35+
/** quoted fees */
36+
executionFee = 0;
37+
worstChainFee = 0;
38+
quoteLoading = false;
39+
3440
constructor(store: Store) {
3541
makeAutoObservable(this, {}, { deep: false, autoBind: true });
3642

@@ -184,22 +190,27 @@ export default class OrderFormView {
184190

185191
setAmount(amount: number) {
186192
this.amount = amount;
193+
this.fetchQuote();
187194
}
188195

189196
setPremium(premium: number) {
190197
this.premium = premium;
198+
this.fetchQuote();
191199
}
192200

193201
setDuration(duration: LeaseDuration) {
194202
this.duration = duration;
203+
this.fetchQuote();
195204
}
196205

197206
setMinChanSize(minChanSize: number) {
198207
this.minChanSize = minChanSize;
208+
this.fetchQuote();
199209
}
200210

201211
setMaxBatchFeeRate(feeRate: number) {
202212
this.maxBatchFeeRate = feeRate;
213+
this.fetchQuote();
203214
}
204215

205216
setMinNodeTier(minNodeTier: Tier) {
@@ -220,6 +231,7 @@ export default class OrderFormView {
220231
const suggested = this.amount * prevPctRate;
221232
// round to the nearest 10 to offset lose of precision in calculating percentages
222233
this.premium = Math.round(suggested / 10) * 10;
234+
this.fetchQuote();
223235
} catch (error) {
224236
this._store.appView.handleError(error, 'Unable to suggest premium');
225237
}
@@ -229,6 +241,51 @@ export default class OrderFormView {
229241
this.addlOptionsVisible = !this.addlOptionsVisible;
230242
}
231243

244+
/** requests a quote for an order to obtain accurate fees */
245+
async quoteOrder() {
246+
const minUnitsMatch = Math.floor(this.minChanSize / ONE_UNIT);
247+
const satsPerKWeight = this._store.api.pool.satsPerVByteToKWeight(
248+
this.maxBatchFeeRate,
249+
);
250+
251+
const {
252+
totalExecutionFeeSat,
253+
worstCaseChainFeeSat,
254+
} = await this._store.orderStore.quoteOrder(
255+
this.amount,
256+
this.perBlockFixedRate,
257+
this.derivedDuration,
258+
minUnitsMatch,
259+
satsPerKWeight,
260+
);
261+
262+
runInAction(() => {
263+
this.executionFee = totalExecutionFeeSat;
264+
this.worstChainFee = worstCaseChainFeeSat;
265+
this.quoteLoading = false;
266+
});
267+
}
268+
269+
/** quote order at most once every second when using this func */
270+
quoteOrderThrottled = debounce(this.quoteOrder, 1000);
271+
272+
/**
273+
* sets the quoteLoading flag before while waiting for the throttled quote
274+
* request to complete
275+
*/
276+
fetchQuote() {
277+
if (!this.isValid) {
278+
runInAction(() => {
279+
this.executionFee = 0;
280+
this.worstChainFee = 0;
281+
this.quoteLoading = false;
282+
});
283+
return;
284+
}
285+
this.quoteLoading = true;
286+
this.quoteOrderThrottled();
287+
}
288+
232289
/** submits the order to the API and resets the form values if successful */
233290
async placeOrder() {
234291
const minUnitsMatch = Math.floor(this.minChanSize / ONE_UNIT);
@@ -249,6 +306,8 @@ export default class OrderFormView {
249306
this.amount = 0;
250307
this.premium = 0;
251308
this.duration = 0;
309+
this.executionFee = 0;
310+
this.worstChainFee = 0;
252311
// persist the additional options so they can be used for future orders
253312
this._store.settingsStore.setOrderSettings(
254313
this.minChanSize,

app/src/util/tests/sampleData.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,14 @@ export const poolListOrders: POOL.ListOrdersResponse.AsObject = {
542542
],
543543
};
544544

545+
export const poolQuoteOrder: POOL.QuoteOrderResponse.AsObject = {
546+
ratePerBlock: 0.00000248,
547+
ratePercent: 0.000248,
548+
totalExecutionFeeSat: 5001,
549+
totalPremiumSat: 24998,
550+
worstCaseChainFeeSat: 40810,
551+
};
552+
545553
export const poolSubmitOrder: POOL.SubmitOrderResponse.AsObject = {
546554
acceptedOrderNonce: 'W4XLkXhEKMcKfzV+Ex+jXQJeaVXoCoKQzptMRi6g+ZA=',
547555
};
@@ -811,6 +819,7 @@ export const sampleApiResponses: Record<string, any> = {
811819
'poolrpc.Trader.DepositAccount': poolDepositAccount,
812820
'poolrpc.Trader.WithdrawAccount': poolWithdrawAccount,
813821
'poolrpc.Trader.ListOrders': poolListOrders,
822+
'poolrpc.Trader.QuoteOrder': poolQuoteOrder,
814823
'poolrpc.Trader.SubmitOrder': poolSubmitOrder,
815824
'poolrpc.Trader.CancelOrder': poolCancelOrder,
816825
'poolrpc.Trader.BatchSnapshot': poolBatchSnapshot,

0 commit comments

Comments
 (0)