Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pool+order: display fee estimates in the order form #243

Merged
merged 1 commit into from
Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/src/api/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,27 @@ class PoolApi extends BaseApi<PoolEvents> {
return res.toObject();
}

/**
* call the pool `QuoteOrder` RPC and return the response
*/
async quoteOrder(
amount: number,
rateFixed: number,
duration: number,
minUnitsMatch: number,
feeRateSatPerKw: number,
): Promise<POOL.QuoteOrderResponse.AsObject> {
const req = new POOL.QuoteOrderRequest();
req.setAmt(amount);
req.setRateFixed(rateFixed);
req.setLeaseDurationBlocks(duration);
req.setMinUnitsMatch(minUnitsMatch);
req.setMaxBatchFeeRateSatPerKw(feeRateSatPerKw);

const res = await this._grpc.request(Trader.QuoteOrder, req, this._meta);
return res.toObject();
}

/**
* call the pool `SubmitOrder` RPC and return the response
*/
Expand Down
34 changes: 34 additions & 0 deletions app/src/components/pool/OrderFormSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { LeaseDuration } from 'types/state';
import Big from 'big.js';
import { usePrefixedTranslation } from 'hooks';
import { Unit, Units } from 'util/constants';
import { useStore } from 'store';
Expand All @@ -18,8 +19,11 @@ import BlockTime from 'components/common/BlockTime';
import FormField from 'components/common/FormField';
import FormInputNumber from 'components/common/FormInputNumber';
import FormSelect from 'components/common/FormSelect';
import LoaderLines from 'components/common/LoaderLines';
import StatusDot from 'components/common/StatusDot';
import Tip from 'components/common/Tip';
import Toggle from 'components/common/Toggle';
import UnitCmp from 'components/common/Unit';
import { styled } from 'components/theme';

const Styled = {
Expand Down Expand Up @@ -62,6 +66,11 @@ const Styled = {
margin: 30px auto;
text-align: center;
`,
LoaderLines: styled(LoaderLines)`
.line {
margin: 0 1px;
}
`,
};

const OrderFormSection: React.FC = () => {
Expand All @@ -80,6 +89,7 @@ const OrderFormSection: React.FC = () => {
OptionsStatus,
Divider,
Actions,
LoaderLines,
} = Styled;
return (
<Section>
Expand Down Expand Up @@ -177,6 +187,30 @@ const OrderFormSection: React.FC = () => {
/>
</OptionsButton>
<Divider />
<Tip overlay={l('executionFeeTip')} capitalize={false} placement="topRight">
<SummaryItem>
<span>{l('executionFeeLabel')}</span>
<span>
{orderFormView.quoteLoading ? (
<LoaderLines />
) : (
<UnitCmp sats={Big(orderFormView.executionFee)} />
)}
</span>
</SummaryItem>
</Tip>
<SummaryItem>
<Tip overlay={l('chainFeeTip')} capitalize={false} placement="topRight">
<span>{l('chainFeeLabel')}</span>
</Tip>
<span>
{orderFormView.quoteLoading ? (
<LoaderLines />
) : (
<UnitCmp sats={Big(orderFormView.worstChainFee)} />
)}
</span>
</SummaryItem>
<SummaryItem>
<span>{l('durationLabel')}</span>
<span className="text-right">
Expand Down
4 changes: 4 additions & 0 deletions app/src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@
"cmps.pool.OrderFormSection.feeLabel": "Max Batch Fee Rate",
"cmps.pool.OrderFormSection.feePlaceholder": "100",
"cmps.pool.OrderFormSection.tierLabel": "Min Node Tier",
"cmps.pool.OrderFormSection.executionFeeLabel": "Execution Fee",
"cmps.pool.OrderFormSection.executionFeeTip": "Total fee paid to the auctioneer for executing this order",
"cmps.pool.OrderFormSection.chainFeeLabel": "Worst Case Chain Fee",
"cmps.pool.OrderFormSection.chainFeeTip": "Assumes chain fees for the footprint of (amount / min_chan_size) channel openings using the max_batch_fee_rate",
"cmps.pool.OrderFormSection.fixedRateLabel": "Per Block Fixed Rate",
"cmps.pool.OrderFormSection.interestLabel": "Interest Rate",
"cmps.pool.OrderFormSection.aprLabel": "Annual Rate (APR)",
Expand Down
44 changes: 44 additions & 0 deletions app/src/store/stores/orderStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,50 @@ export default class OrderStore {
/** fetch leases at most once every 2 seconds when using this func */
fetchLeasesThrottled = debounce(this.fetchLeases, 2000);

/**
* Requests a fee quote for an order
* @param amount the amount of the order
* @param rateFixed the per block fixed rate
* @param duration the number of blocks to keep the channel open for
* @param minUnitsMatch the minimum number of units required to match this order
* @param maxBatchFeeRate the maximum batch fee rate to allowed as sats per vByte
*/
async quoteOrder(
amount: number,
rateFixed: number,
duration: number,
minUnitsMatch: number,
maxBatchFeeRate: number,
): Promise<POOL.QuoteOrderResponse.AsObject> {
try {
this._store.log.info(`quoting an order for ${amount}sats`, {
rateFixed,
duration,
minUnitsMatch,
maxBatchFeeRate,
});

const res = await this._store.api.pool.quoteOrder(
amount,
rateFixed,
duration,
minUnitsMatch,
maxBatchFeeRate,
);

return res;
} catch (error) {
this._store.appView.handleError(error, 'Unable to estimate order fees');
return {
ratePerBlock: rateFixed,
ratePercent: 0,
totalExecutionFeeSat: 0,
totalPremiumSat: 0,
worstCaseChainFeeSat: 0,
};
}
}

/**
* Submits an order to the market
* @param type the type of order (bid or ask)
Expand Down
59 changes: 59 additions & 0 deletions app/src/store/views/orderFormView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
NodeTier,
} from 'types/generated/auctioneerrpc/auctioneer_pb';
import { LeaseDuration } from 'types/state';
import debounce from 'lodash/debounce';
import { annualPercentRate, toBasisPoints, toPercent } from 'util/bigmath';
import { BLOCKS_PER_DAY } from 'util/constants';
import { prefixTranslation } from 'util/translate';
Expand Down Expand Up @@ -31,6 +32,11 @@ export default class OrderFormView {
/** toggle to show or hide the additional options */
addlOptionsVisible = false;

/** quoted fees */
executionFee = 0;
worstChainFee = 0;
quoteLoading = false;

constructor(store: Store) {
makeAutoObservable(this, {}, { deep: false, autoBind: true });

Expand Down Expand Up @@ -184,22 +190,27 @@ export default class OrderFormView {

setAmount(amount: number) {
this.amount = amount;
this.fetchQuote();
}

setPremium(premium: number) {
this.premium = premium;
this.fetchQuote();
}

setDuration(duration: LeaseDuration) {
this.duration = duration;
this.fetchQuote();
}

setMinChanSize(minChanSize: number) {
this.minChanSize = minChanSize;
this.fetchQuote();
}

setMaxBatchFeeRate(feeRate: number) {
this.maxBatchFeeRate = feeRate;
this.fetchQuote();
}

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

/** requests a quote for an order to obtain accurate fees */
async quoteOrder() {
const minUnitsMatch = Math.floor(this.minChanSize / ONE_UNIT);
const satsPerKWeight = this._store.api.pool.satsPerVByteToKWeight(
this.maxBatchFeeRate,
);

const {
totalExecutionFeeSat,
worstCaseChainFeeSat,
} = await this._store.orderStore.quoteOrder(
this.amount,
this.perBlockFixedRate,
this.derivedDuration,
minUnitsMatch,
satsPerKWeight,
);

runInAction(() => {
this.executionFee = totalExecutionFeeSat;
this.worstChainFee = worstCaseChainFeeSat;
this.quoteLoading = false;
});
}

/** quote order at most once every second when using this func */
quoteOrderThrottled = debounce(this.quoteOrder, 1000);

/**
* sets the quoteLoading flag before while waiting for the throttled quote
* request to complete
*/
fetchQuote() {
if (!this.isValid) {
runInAction(() => {
this.executionFee = 0;
this.worstChainFee = 0;
this.quoteLoading = false;
});
return;
}
this.quoteLoading = true;
this.quoteOrderThrottled();
}

/** submits the order to the API and resets the form values if successful */
async placeOrder() {
const minUnitsMatch = Math.floor(this.minChanSize / ONE_UNIT);
Expand All @@ -249,6 +306,8 @@ export default class OrderFormView {
this.amount = 0;
this.premium = 0;
this.duration = 0;
this.executionFee = 0;
this.worstChainFee = 0;
// persist the additional options so they can be used for future orders
this._store.settingsStore.setOrderSettings(
this.minChanSize,
Expand Down
9 changes: 9 additions & 0 deletions app/src/util/tests/sampleData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,14 @@ export const poolListOrders: POOL.ListOrdersResponse.AsObject = {
],
};

export const poolQuoteOrder: POOL.QuoteOrderResponse.AsObject = {
ratePerBlock: 0.00000248,
ratePercent: 0.000248,
totalExecutionFeeSat: 5001,
totalPremiumSat: 24998,
worstCaseChainFeeSat: 40810,
};

export const poolSubmitOrder: POOL.SubmitOrderResponse.AsObject = {
acceptedOrderNonce: 'W4XLkXhEKMcKfzV+Ex+jXQJeaVXoCoKQzptMRi6g+ZA=',
};
Expand Down Expand Up @@ -811,6 +819,7 @@ export const sampleApiResponses: Record<string, any> = {
'poolrpc.Trader.DepositAccount': poolDepositAccount,
'poolrpc.Trader.WithdrawAccount': poolWithdrawAccount,
'poolrpc.Trader.ListOrders': poolListOrders,
'poolrpc.Trader.QuoteOrder': poolQuoteOrder,
'poolrpc.Trader.SubmitOrder': poolSubmitOrder,
'poolrpc.Trader.CancelOrder': poolCancelOrder,
'poolrpc.Trader.BatchSnapshot': poolBatchSnapshot,
Expand Down