Skip to content

Commit 6f7075d

Browse files
committed
pool+order: display fee estimates in the order form
1 parent 104591e commit 6f7075d

File tree

6 files changed

+142
-0
lines changed

6 files changed

+142
-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: 19 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';
@@ -19,7 +20,9 @@ import FormField from 'components/common/FormField';
1920
import FormInputNumber from 'components/common/FormInputNumber';
2021
import FormSelect from 'components/common/FormSelect';
2122
import StatusDot from 'components/common/StatusDot';
23+
import Tip from 'components/common/Tip';
2224
import Toggle from 'components/common/Toggle';
25+
import UnitCmp from 'components/common/Unit';
2326
import { styled } from 'components/theme';
2427

2528
const Styled = {
@@ -177,6 +180,22 @@ const OrderFormSection: React.FC = () => {
177180
/>
178181
</OptionsButton>
179182
<Divider />
183+
<Tip overlay={l('executionFeeTip')} capitalize={false} placement="topRight">
184+
<SummaryItem>
185+
<span>{l('executionFeeLabel')}</span>
186+
<span>
187+
<UnitCmp sats={Big(orderFormView.executionFee)} />
188+
</span>
189+
</SummaryItem>
190+
</Tip>
191+
<SummaryItem>
192+
<Tip overlay={l('chainFeeTip')} capitalize={false} placement="topRight">
193+
<span>{l('chainFeeLabel')}</span>
194+
</Tip>
195+
<span>
196+
<UnitCmp sats={Big(orderFormView.worstChainFee)} />
197+
</span>
198+
</SummaryItem>
180199
<SummaryItem>
181200
<span>{l('durationLabel')}</span>
182201
<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: 45 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,10 @@ 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+
3439
constructor(store: Store) {
3540
makeAutoObservable(this, {}, { deep: false, autoBind: true });
3641

@@ -184,22 +189,27 @@ export default class OrderFormView {
184189

185190
setAmount(amount: number) {
186191
this.amount = amount;
192+
this.quoteOrderThrottled();
187193
}
188194

189195
setPremium(premium: number) {
190196
this.premium = premium;
197+
this.quoteOrderThrottled();
191198
}
192199

193200
setDuration(duration: LeaseDuration) {
194201
this.duration = duration;
202+
this.quoteOrderThrottled();
195203
}
196204

197205
setMinChanSize(minChanSize: number) {
198206
this.minChanSize = minChanSize;
207+
this.quoteOrderThrottled();
199208
}
200209

201210
setMaxBatchFeeRate(feeRate: number) {
202211
this.maxBatchFeeRate = feeRate;
212+
this.quoteOrderThrottled();
203213
}
204214

205215
setMinNodeTier(minNodeTier: Tier) {
@@ -229,6 +239,39 @@ export default class OrderFormView {
229239
this.addlOptionsVisible = !this.addlOptionsVisible;
230240
}
231241

242+
/** requests a quote for an order to obtain accurate fees */
243+
async quoteOrder() {
244+
runInAction(() => {
245+
this.executionFee = 0;
246+
this.worstChainFee = 0;
247+
});
248+
if (!this.isValid) return;
249+
250+
const minUnitsMatch = Math.floor(this.minChanSize / ONE_UNIT);
251+
const satsPerKWeight = this._store.api.pool.satsPerVByteToKWeight(
252+
this.maxBatchFeeRate,
253+
);
254+
255+
const {
256+
totalExecutionFeeSat,
257+
worstCaseChainFeeSat,
258+
} = await this._store.orderStore.quoteOrder(
259+
this.amount,
260+
this.perBlockFixedRate,
261+
this.derivedDuration,
262+
minUnitsMatch,
263+
satsPerKWeight,
264+
);
265+
266+
runInAction(() => {
267+
this.executionFee = totalExecutionFeeSat;
268+
this.worstChainFee = worstCaseChainFeeSat;
269+
});
270+
}
271+
272+
/** quote order at most once every 2 seconds when using this func */
273+
quoteOrderThrottled = debounce(this.quoteOrder, 2000);
274+
232275
/** submits the order to the API and resets the form values if successful */
233276
async placeOrder() {
234277
const minUnitsMatch = Math.floor(this.minChanSize / ONE_UNIT);
@@ -249,6 +292,8 @@ export default class OrderFormView {
249292
this.amount = 0;
250293
this.premium = 0;
251294
this.duration = 0;
295+
this.executionFee = 0;
296+
this.worstChainFee = 0;
252297
// persist the additional options so they can be used for future orders
253298
this._store.settingsStore.setOrderSettings(
254299
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)