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

Support fees in Penumbra web #362

Merged
merged 6 commits into from
Jan 31, 2024
Merged

Conversation

jessepinho
Copy link
Contributor

@jessepinho jessepinho commented Jan 8, 2024

Background

Per #308, we want to support transaction fees in Penumbra Web. To do this, we need to make a TransactionPlannerRequest each time one of the values in the send form changes, then grab the gas fee off of the returned plan.

In this PR

  • Refactor planWitnessBuildBroadcast in the SendSlice to extract a getPlan function that takes care of just the planning portion.
  • Use that getPlan function to get the gas fee any time inputs change in the send form.
  • Display the gas fee below the amount to send.
  • Hardcode an undefined gas fee for the IBC form, which we're not yet using (per Henry).

Closes #308

...props
}: InputTokenProps) {
const vResult = validationResult(value, validations);

const currentBalance = getCurrentBalance(selection?.asset);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted to a helper to make it clear what this represents.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got this from Lucide, which Zpoken told us is where we got some of our icons.

It'd be great to eventually standardize an icon set, though — right now, we seem to be getting icons from Radix, Lucide, and our Figma mockups (and I don't know where those mockups' icons come from).

@@ -82,7 +89,7 @@ export const SendForm = () => {
},
]}
balances={accountBalances}
tempPrice={1}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my note below about removing the temp price and its corresponding UI

import type { Impl } from '.';

export const gasPrices: Impl['gasPrices'] = () => {
/** @todo Replace this stub with real gas prices. */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Valentine1898 is currently working on this as part of #203

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that gas prices do not actually change on the network and are always equal to these values

blockSpacePrice: 0n
compactBlockSpacePrice: 0n
executionPrice: 0n
verificationPrice: 0n

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Valentine1898 but that could change in the future, right? i.e., I should still be requesting them (using your new implementation) rather rather than depending on all-0 values?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was just clarifying that we're currently getting all the 0 values from the network

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, thanks

Comment on lines 168 to 173
return (
gasPrices.blockSpacePrice +
gasPrices.compactBlockSpacePrice +
gasPrices.verificationPrice +
gasPrices.executionPrice
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm summing up the different dimensions of the gas price to get the total. I'm not 100% certain that this logic is correct — this is where I'm bumping against the limits of my crypto knowledge — so reviewers, please let me know if I've got this right.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like the final step of calculating a total might be the location where the denominator is expected to be considered

https://github.com/penumbra-zone/penumbra/blob/main/crates/core/component/fee/src/gas.rs#L65-L74

)}
>
${displayAmount(Number(value) * tempPrice)}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, this appeared to be a stub of the gas price. (I'm not sure of that, though — I'm not actually sure what tempPrice was for.) So I removed it and displayed the gas price in its place. If I'm missing something about the tempPrice, let me know.

Copy link
Contributor

@turbocrime turbocrime left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the way i would handle this, is instead of having a state selector that provides the total, provide a state selector that provides all of the price components. that way it could be displayed differently, or with more detail.

otherwise looks like good progress, pending the wasm impl

Comment on lines 168 to 173
return (
gasPrices.blockSpacePrice +
gasPrices.compactBlockSpacePrice +
gasPrices.verificationPrice +
gasPrices.executionPrice
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like the final step of calculating a total might be the location where the denominator is expected to be considered

https://github.com/penumbra-zone/penumbra/blob/main/crates/core/component/fee/src/gas.rs#L65-L74

@jessepinho jessepinho force-pushed the jessepinho/web-308-gas-fees-in-web branch from 403fb83 to 6163c32 Compare January 9, 2024 22:10
@turbocrime
Copy link
Contributor

how to calculate gas price preview on send form

gas prices can be calculated from transaction plan information. the planner performs this calculation here

https://github.com/penumbra-zone/penumbra/blob/main/crates/view/src/planner.rs#L157-L174

the transactionplan's impl simply sums the costs of the action plans

https://github.com/penumbra-zone/penumbra/blob/main/crates/core/transaction/src/gas.rs#L225-L229

the send form only ever generates 'spend' and 'output' actions. cost for these plans are constant, based on their constant dimensions

https://github.com/penumbra-zone/penumbra/blob/main/crates/core/transaction/src/gas.rs#L23-L65

the final cost can be estimated using price information from GasPriceRequest https://github.com/penumbra-zone/penumbra/blob/main/crates/core/component/fee/src/gas.rs#L65-L74

@hdevalence
Copy link
Member

hdevalence commented Jan 10, 2024

how to calculate gas price preview on send form

It'd be strongly preferable not to replicate gas cost estimation logic in typescript. We will already be doing exact gas costing as part of the planner. Can we instead just read the fee out of the returned transaction plan?

It is actually reasonably complicated to estimate the gas cost for a particular action, because the frontend isn't in a position to know e.g., how many notes need to be spent to create a plan that fufils the user's intent. (What if a user's balance is spread over many small notes)? Implementing all of this is fairly close to implementing the transaction planner logic itself.

Instead, what if the form submitted planner requests to the extension as the values were input, so it can display a preview, but have the logic for computing the preview be the actual logic that will be used for tx planning?

@turbocrime
Copy link
Contributor

the ticket requested displaying a preview in the webapp, by our interpretation. displaying in the popup is certainly much easier :)

@turbocrime
Copy link
Contributor

or, should we have an extra stage between "plan came back" and "submit plan for auth"

@hdevalence
Copy link
Member

@turbocrime added an extra suggestion to my comment in an edit, i think a preview is certainly nice to have, wdyt of that strategy for having it?

the point is that the view service methods don't have user in the loop, the page can make as many planner requests as it wants and throw away the resulting plans

@turbocrime
Copy link
Contributor

Instead, what if the form submitted planner requests to the extension as the values were input, so it can display a preview, but have the logic for computing the preview be the actual logic that will be used for tx planning?

current design would require sending a new plan request on every change, creating a new planner and re-planning the whole tx each time, instead of incrementally planning a single tx

@turbocrime
Copy link
Contributor

very doable

@hdevalence
Copy link
Member

my assumption is that planning should be pretty fast, fast enough that we could just spam requests on form change, it seems at least worth trying

@turbocrime
Copy link
Contributor

the existing webapp combines plan/witness/build/broadcast into a single fn and i think we were conceptually trapped lol

@Valentine1898
Copy link
Contributor

the existing webapp combines plan/witness/build/broadcast into a single fn and i think we were conceptually trapped lol

Maybe it makes sense to leave it that way and send separate planner requests for gas cost preview

@jessepinho
Copy link
Contributor Author

Thanks all — making planner requests for the gas costs sounds great. I'll go with that approach!

@jessepinho jessepinho force-pushed the jessepinho/web-308-gas-fees-in-web branch 3 times, most recently from bbd97d7 to 3975294 Compare January 10, 2024 20:14
import { useStore } from '../../../state';

/**
* Refreshes the fee in the state when the amount, recipient, selection, or memo
Copy link
Contributor Author

@jessepinho jessepinho Jan 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we're refreshing even when just the memo changes. Am I correct in understanding that a larger memo could result in the fee changing, since a larger memo takes up more space? Or does the fee remain the same regardless of memo?

If the latter, I'll remove memo from the list of dependencies on line 14 below, since we don't need to refresh the fee when the memo changes.

Copy link
Contributor

@turbocrime turbocrime Jan 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe an encrypted memo is constant size, even if it contains no memo

additionally, when the planner calculates tx cost it does not use the memo, it uses the list of actions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks — I've removed memo from the effect dependencies.

import { Selection } from '../../state/types';
import { Fee } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/fee/v1alpha1/fee_pb';

const PENUMBRA_FEE_DENOMINATOR = 1000;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we're currently hardcoding the penumbra fee denominator. @Valentine1898 pointed out that I could call getAllAssets to get the denominator of the token referenced via the fee's assetId property; but @hdevalence, will there ever be a case where the fee will be paid in something other than the staking token (penumbra)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe currently tx fees are always PEN https://protocol.penumbra.zone/main/stake/validator-rewards.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!selection.asset) throw new Error('no selected asset');
type TransactionProps = Pick<SendSlice, 'amount' | 'recipient' | 'selection' | 'memo'>;

const getPlan = async ({ amount, recipient, selection, memo }: TransactionProps) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted a getPlan function, which we call to refresh the fee whenever a form input changes.

return plan;
};

const planWitnessBuildBroadcast = async ({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works exactly as before, even throwing the same errors as before. I've only extracted a getPlan function above.

@jessepinho jessepinho force-pushed the jessepinho/web-308-gas-fees-in-web branch from 8191b14 to 96585fa Compare January 10, 2024 21:11
@jessepinho jessepinho marked this pull request as ready for review January 10, 2024 21:18
@jessepinho jessepinho changed the title DRAFT: Support fees in Penumbra web Support fees in Penumbra web Jan 10, 2024
import { Selection } from '../../state/types';
import { Fee } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/fee/v1alpha1/fee_pb';

const PENUMBRA_FEE_DENOMINATOR = 1000;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jessepinho jessepinho force-pushed the jessepinho/web-308-gas-fees-in-web branch from 31e2ace to c253544 Compare January 26, 2024 04:57
@jessepinho jessepinho force-pushed the jessepinho/web-308-gas-fees-in-web branch 3 times, most recently from 1aebc54 to f389249 Compare January 31, 2024 07:11
Copy link
Collaborator

@grod220 grod220 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments, feel free to merge after 👍

Comment on lines +11 to +28
export const useRefreshFee = () => {
const { amount, recipient, selection, refreshFee } = useStore(sendSelector);
const timeoutId = useRef<number | null>(null);

const debouncedRefreshFee = useCallback(() => {
if (timeoutId.current) {
window.clearTimeout(timeoutId.current);
timeoutId.current = null;
}

timeoutId.current = window.setTimeout(() => {
timeoutId.current = null;
void refreshFee();
}, DEBOUNCE_MS);
}, [refreshFee]);

useEffect(debouncedRefreshFee, [amount, recipient, selection, debouncedRefreshFee]);
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like it may be better as a selector. Maybe not though 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selectors always have to be synchronous, pure functions, no? Or how are you suggesting accomplishing this with a selector?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed in Discord — this will be part of a follow-up PR

Comment on lines +17 to +20
const getCurrentBalance = (assetBalance: AssetBalance | undefined) =>
assetBalance
? fromBaseUnitAmount(assetBalance.amount, assetBalance.denom.exponent).toFormat()
: '0';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use ValueViewComponent here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed in Discord — this will be part of a follow-up PR (in progress here #440)

Comment on lines 10 to 12
const fee = joinLoHiAmount(txv.bodyView.transactionParameters!.fee!.amount!).toString();

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we should have error guards here:

  if (!txv.bodyView?.transactionParameters?.fee?.amount) throw new Error('Missing fee amount');
  const fee = joinLoHiAmount(txv.bodyView.transactionParameters.fee.amount).toString();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — fixed

@jessepinho jessepinho force-pushed the jessepinho/web-308-gas-fees-in-web branch from f0efaad to 72a046c Compare January 31, 2024 19:00
@jessepinho jessepinho merged commit c16f001 into main Jan 31, 2024
3 checks passed
@jessepinho jessepinho deleted the jessepinho/web-308-gas-fees-in-web branch January 31, 2024 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support fees in Penumbra Web
5 participants