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

fees: ux for multi-asset fees #1268

Merged
merged 29 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f0b146f
add index for spendable notes table in idb
TalDerei Jun 9, 2024
1f44143
scaffolding planner support for multi-asset fees
TalDerei Jun 9, 2024
c92ddfa
filter for alt fee asset id
TalDerei Jun 10, 2024
837d72b
fix fee rendering
TalDerei Jun 10, 2024
e8e0538
full multi-asset fee support ~
TalDerei Jun 10, 2024
6f0244e
attempt to pass CI
TalDerei Jun 10, 2024
45bfe51
Merge branch 'main' into ux-multi-fees
TalDerei Jun 10, 2024
c1b2aee
partially address comments
TalDerei Jun 12, 2024
23b6e19
valentine + jesse feedback
TalDerei Jun 13, 2024
fbc25cb
merge main
TalDerei Jun 13, 2024
d9b7373
fix broken lockfile, update deps, update db version
TalDerei Jun 13, 2024
f53027a
remove dangling minifront_url
TalDerei Jun 13, 2024
6532dbc
co-locate idb version with storage package
TalDerei Jun 13, 2024
f11da6a
remove idb version from extension .env
TalDerei Jun 13, 2024
89a24c1
support actions for alt fees
TalDerei Jun 14, 2024
ccfe1cf
linting and organization cleanup
TalDerei Jun 14, 2024
81e04bb
merge main post prax migration
TalDerei Jun 14, 2024
92e3dbd
update lockfile
TalDerei Jun 14, 2024
12afb80
Merge branch 'main' into ux-multi-fees
TalDerei Jun 15, 2024
1c8a6a4
remove extension dir trace
TalDerei Jun 15, 2024
2b60977
fix lockfile?
TalDerei Jun 15, 2024
b20ea66
address valentine comments
TalDerei Jun 17, 2024
0717d90
merge main
TalDerei Jun 17, 2024
04a0d65
fix test suite
TalDerei Jun 17, 2024
8721bc2
try fixing rust tests
Valentine1898 Jun 17, 2024
e570f2e
rust lint
Valentine1898 Jun 17, 2024
71ce2d3
rust lint
Valentine1898 Jun 17, 2024
3e9101f
add TODOs for #1310
Valentine1898 Jun 17, 2024
5de82b8
fix lint
Valentine1898 Jun 17, 2024
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
2 changes: 1 addition & 1 deletion apps/extension/.env.testnet
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
IDB_VERSION=42
IDB_VERSION=43
MINIFRONT_URL=https://app.testnet.penumbra.zone
PRAX=lkpmkhpnhknhmibgnmmhdhgdilepfghe
1 change: 1 addition & 0 deletions packages/services/src/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface IndexedDbMock {
getPricesForAsset?: Mock;
getAuction?: Mock;
getAuctionOutstandingReserves?: Mock;
hasNativeAssetBalance?: Mock;
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
}

export interface AuctionMock {
Expand Down
40 changes: 40 additions & 0 deletions packages/services/src/view-service/fees.ts
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { AssetId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import {
TransactionPlannerRequest,
TransactionPlannerRequest_Output,
TransactionPlannerRequest_Swap,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';

export const extractAltFee = (request: TransactionPlannerRequest): AssetId | undefined => {
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
const fields = [
{ name: 'outputs', value: request.outputs },
{ name: 'swaps', value: request.swaps },
{ name: 'swapClaims', value: request.swapClaims },
{ name: 'delegations', value: request.delegations },
{ name: 'undelegations', value: request.undelegations },
{ name: 'undelegationClaims', value: request.undelegationClaims },
{ name: 'ibcRelayActions', value: request.ibcRelayActions },
{ name: 'ics20Withdrawals', value: request.ics20Withdrawals },
{ name: 'positionOpens', value: request.positionOpens },
{ name: 'positionCloses', value: request.positionCloses },
{ name: 'positionWithdraws', value: request.positionWithdraws },
{ name: 'dutchAuctionScheduleActions', value: request.dutchAuctionScheduleActions },
];

const nonEmptyField = fields.find(field => field.value.length > 0);

if (!nonEmptyField) {
console.warn('No non-empty field found in the request');
return undefined;
}

type PossibleTypes = TransactionPlannerRequest_Output | TransactionPlannerRequest_Swap;
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

const action = nonEmptyField.value[0] as PossibleTypes;
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

if (!action.value?.assetId) {
return undefined;
}

return action.value.assetId;
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('TransactionPlanner request handler', () => {
getAppParams: vi.fn(),
getGasPrices: vi.fn(),
constants: vi.fn(),
hasNativeAssetBalance: vi.fn(),
};

mockServices = {
Expand Down Expand Up @@ -77,47 +78,9 @@ describe('TransactionPlanner request handler', () => {
compactBlockSpacePrice: 120n,
}),
);
mockIndexedDb.hasNativeAssetBalance?.mockResolvedValueOnce(true);
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
await transactionPlanner(req, mockCtx);

expect(mockPlanTransaction.mock.calls.length === 1).toBeTruthy();
});

test('should throw error if FmdParameters not available', async () => {
await expect(transactionPlanner(req, mockCtx)).rejects.toThrow('FmdParameters not available');
});

test('should throw error if SctParameters not available', async () => {
mockIndexedDb.getFmdParams?.mockResolvedValueOnce(new FmdParameters());
mockIndexedDb.getAppParams?.mockResolvedValueOnce(
new AppParameters({
chainId: 'penumbra-testnet-mock',
}),
);
await expect(transactionPlanner(req, mockCtx)).rejects.toThrow('SctParameters not available');
});

test('should throw error if ChainId not available', async () => {
mockIndexedDb.getFmdParams?.mockResolvedValueOnce(new FmdParameters());
mockIndexedDb.getAppParams?.mockResolvedValueOnce(
new AppParameters({
sctParams: new SctParameters({
epochDuration: 719n,
}),
}),
);
await expect(transactionPlanner(req, mockCtx)).rejects.toThrow('ChainId not available');
});

test('should throw error if Gas prices is not available', async () => {
mockIndexedDb.getFmdParams?.mockResolvedValueOnce(new FmdParameters());
mockIndexedDb.getAppParams?.mockResolvedValueOnce(
new AppParameters({
chainId: 'penumbra-testnet-mock',
sctParams: new SctParameters({
epochDuration: 719n,
}),
}),
);
await expect(transactionPlanner(req, mockCtx)).rejects.toThrow('Gas prices is not available');
});
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
});
21 changes: 20 additions & 1 deletion packages/services/src/view-service/transaction-planner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,30 @@ import { Code, ConnectError } from '@connectrpc/connect';
import { assertSwapAssetsAreNotTheSame } from './assert-swap-assets-are-not-the-same';
import { TransactionPlannerRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import { fvkCtx } from '../../ctx/full-viewing-key';
import { extractAltFee } from '../fees';
import { AssetId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';

export const transactionPlanner: Impl['transactionPlanner'] = async (req, ctx) => {
const services = await ctx.values.get(servicesCtx)();
const { indexedDb } = await services.getWalletServices();

// Retrieve the staking token from asset registry
const stakingTokenId = await indexedDb.fetchStakingTokenId();

// Query IndexedDB directly to check for the existence of staking token
const nativeToken = await indexedDb.hasStakingAssetBalance(stakingTokenId);

// Initialize the gas fee token using an native staking token's asset ID
let gasFeeToken = new AssetId({
inner: stakingTokenId.inner,
});

// If there is no native token balance, extract and use an alternate gas fee token
// from the transaction request
if (!nativeToken) {
gasFeeToken = extractAltFee(req)!;
}

const fvk = ctx.values.get(fvkCtx);

assertValidRequest(req);
Expand All @@ -24,7 +43,7 @@ export const transactionPlanner: Impl['transactionPlanner'] = async (req, ctx) =

const idbConstants = indexedDb.constants();

const plan = await planTransaction(idbConstants, req, await fvk());
const plan = await planTransaction(idbConstants, req, await fvk(), gasFeeToken);
return { plan };
};

Expand Down
73 changes: 53 additions & 20 deletions packages/storage/src/indexed-db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ import {
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/auction/v1/auction_pb';
import { ChainRegistryClient } from '@penumbra-labs/registry';
import { PartialMessage } from '@bufbuild/protobuf';
import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb';
import { addLoHi } from '@penumbra-zone/types/lo-hi';

interface IndexedDbProps {
idbVersion: number; // Incremented during schema changes
Expand Down Expand Up @@ -100,9 +102,11 @@ export class IndexedDb implements IndexedDbInterface {

db.createObjectStore('FULL_SYNC_HEIGHT');
db.createObjectStore('ASSETS', { keyPath: 'penumbraAssetId.inner' });
db.createObjectStore('SPENDABLE_NOTES', {
const spendableNoteStore = db.createObjectStore('SPENDABLE_NOTES', {
keyPath: 'noteCommitment.inner',
}).createIndex('nullifier', 'nullifier.inner');
});
spendableNoteStore.createIndex('nullifier', 'nullifier.inner');
spendableNoteStore.createIndex('assetId', 'note.value.assetId.inner');
db.createObjectStore('TRANSACTIONS', { keyPath: 'id.inner' });
db.createObjectStore('TREE_LAST_POSITION');
db.createObjectStore('TREE_LAST_FORGOTTEN');
Expand Down Expand Up @@ -140,9 +144,6 @@ export class IndexedDb implements IndexedDbInterface {
const existing0thEpoch = await instance.getEpochByHeight(0n);
if (!existing0thEpoch) await instance.addEpoch(0n); // Create first epoch

// set non-zero gas prices in indexDB since the testnet has not yet enabled gas fees.
await instance.initGasPrices();

return instance;
}

Expand Down Expand Up @@ -285,21 +286,13 @@ export class IndexedDb implements IndexedDbInterface {
);
}

async initGasPrices() {
const savedGasPrices = await this.getGasPrices();
// These are arbitrarily set, but can take on any value.
// The gas prices set here will determine the fees to use Penumbra.
//
// Note: this is a temporary measure to enable gas prices in the web, but once
// https://github.com/penumbra-zone/penumbra/issues/4306 is merged, we can remove this.
if (!savedGasPrices) {
await this.saveGasPrices({
verificationPrice: 1n,
executionPrice: 1n,
blockSpacePrice: 1n,
compactBlockSpacePrice: 1n,
});
}
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
async *iterateSpendableStakingNotes() {
yield* new ReadableStream(
new IdbCursorSource(
this.db.transaction('SPENDABLE_NOTES').store.openCursor(),
SpendableNoteRecord,
),
);
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
}

async *iterateTransactions() {
Expand Down Expand Up @@ -802,4 +795,44 @@ export class IndexedDb implements IndexedDbInterface {
output: Value.fromJson(result.output),
};
}

async fetchStakingTokenId(): Promise<AssetId> {
const registryClient = new ChainRegistryClient();
const registry = registryClient.get(this.chainId);
const stakingTokenMetadata = registry.getMetadata(registry.stakingAssetId);
const stakingToken = getAssetId(stakingTokenMetadata);

return stakingToken;
}

async hasStakingAssetBalance(assetId: AssetId): Promise<boolean> {
const spendableUMNotes = await this.db.getAllFromIndex(
'SPENDABLE_NOTES',
'assetId',
uint8ArrayToBase64(assetId.inner),
);

// Iterate over the spendable UM notes, and acrue balance for unspent notesz
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
let stakingTokenBalance = new Amount();
for (const note of spendableUMNotes) {
let umNote = note as unknown as SpendableNoteRecord;
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
if (umNote.heightSpent == undefined) {
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
const newAmount = addLoHi(
{ lo: stakingTokenBalance.lo, hi: stakingTokenBalance.hi },
{
lo: BigInt(umNote.note?.value?.amount?.lo ?? 0n),
hi: BigInt(umNote.note?.value?.amount?.hi ?? 0n),
},
);
stakingTokenBalance.lo = newAmount.lo;
stakingTokenBalance.hi = newAmount.hi;
}
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
}

if (stakingTokenBalance.lo > 0) {
return true;
}

return false;
}
}
7 changes: 7 additions & 0 deletions packages/types/src/indexed-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export interface IndexedDbInterface {
getAuctionOutstandingReserves(
auctionId: AuctionId,
): Promise<{ input: Value; output: Value } | undefined>;

hasStakingAssetBalance(assetId: AssetId): Promise<boolean>;

fetchStakingTokenId(): Promise<AssetId>;
}

export interface PenumbraDb extends DBSchema {
Expand Down Expand Up @@ -191,6 +195,9 @@ export interface PenumbraDb extends DBSchema {
value: Jsonified<SpendableNoteRecord>;
indexes: {
nullifier: Jsonified<Required<SpendableNoteRecord>['nullifier']['inner']>; // base64
assetId: Jsonified<
Required<Required<Required<SpendableNoteRecord>['note']>['value']>['assetId']['inner']
>; // base64
};
};

Expand Down
4 changes: 1 addition & 3 deletions packages/ui/components/ui/tx/view/swap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ export const SwapViewComponent = ({ value }: { value: SwapView }) => {
)}

<ActionDetails.Row label='Fee'>
<div className='font-mono'>
{joinLoHiAmount(getAmount(claimFee)).toString()} upenumbra
</div>
<div className='font-mono'>{joinLoHiAmount(getAmount(claimFee)).toString()} UM</div>
</ActionDetails.Row>

{claimTx && (
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/components/ui/tx/view/transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export const TransactionViewComponent = ({ txv }: { txv: TransactionView }) => {
if (!txv.bodyView) throw new Error('transaction view missing body view');
if (!txv.bodyView.transactionParameters?.fee?.amount) throw new Error('Missing fee amount');

const fee = joinLoHiAmount(txv.bodyView.transactionParameters.fee.amount).toString();
const fee = (
Number(joinLoHiAmount(txv.bodyView.transactionParameters.fee.amount)) / 1000000
).toString();
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

return (
<div className='flex flex-col gap-8'>
Expand All @@ -19,7 +21,7 @@ export const TransactionViewComponent = ({ txv }: { txv: TransactionView }) => {
))}
</ViewSection>
<ViewSection heading='Parameters'>
<ViewBox label='Fee' visibleContent={<div className='font-mono'>{fee} upenumbra</div>} />
<ViewBox label='Fee' visibleContent={<div className='font-mono'>{fee} UM</div>} />
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
<ViewBox
label='Chain ID'
visibleContent={
Expand Down
22 changes: 20 additions & 2 deletions packages/wasm/crate/src/planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use anyhow::anyhow;
use ark_ff::UniformRand;
use decaf377::{Fq, Fr};
use penumbra_asset::asset::{Id, Metadata};
use penumbra_asset::Value;
use penumbra_asset::{Value, STAKING_TOKEN_ASSET_ID};
use penumbra_auction::auction::dutch::actions::ActionDutchAuctionWithdrawPlan;
use penumbra_auction::auction::dutch::{
ActionDutchAuctionEnd, ActionDutchAuctionSchedule, DutchAuctionDescription,
Expand Down Expand Up @@ -136,6 +136,7 @@ pub async fn plan_transaction(
idb_constants: JsValue,
request: &[u8],
full_viewing_key: &[u8],
gas_fee_token: &[u8],
) -> WasmResult<JsValue> {
utils::set_panic_hook();

Expand Down Expand Up @@ -183,7 +184,8 @@ pub async fn plan_transaction(
..Default::default()
};

let gas_prices: GasPrices = {
// Request information about current gas prices
let mut gas_prices: GasPrices = {
let gas_prices: penumbra_proto::core::component::fee::v1::GasPrices =
serde_wasm_bindgen::from_value(
storage
Expand All @@ -202,6 +204,22 @@ pub async fn plan_transaction(
}
};

// Decode the gas fee token into an `Id` type
let alt_gas: Id = Id::decode(gas_fee_token)?;

// Check if the decoded gas fee token is different from the staking token asset ID.
// If the gas fee token is different, use the alternative gas fee token with a 10x
// multiplier.
if alt_gas != *STAKING_TOKEN_ASSET_ID {
gas_prices = GasPrices {
asset_id: alt_gas,
block_space_price: gas_prices.block_space_price * 10,
compact_block_space_price: gas_prices.compact_block_space_price * 10,
verification_price: gas_prices.verification_price * 10,
execution_price: gas_prices.execution_price * 10,
};
};

let mut actions_list = ActionList::default();

// Phase 1: process all of the user-supplied intents into complete action plans.
Expand Down
11 changes: 11 additions & 0 deletions packages/wasm/crate/tests/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod tests {
use wasm_bindgen::JsValue;
use wasm_bindgen_test::*;

use penumbra_proto::core::asset::v1::AssetId;
use penumbra_wasm::planner::plan_transaction;
use penumbra_wasm::{
build::build_action,
Expand Down Expand Up @@ -427,10 +428,20 @@ mod tests {
// Viewing key to reveal asset balances and transactions.
let full_viewing_key = FullViewingKey::from_str("penumbrafullviewingkey1mnm04x7yx5tyznswlp0sxs8nsxtgxr9p98dp0msuek8fzxuknuzawjpct8zdevcvm3tsph0wvsuw33x2q42e7sf29q904hwerma8xzgrxsgq2").unwrap();

// Native staking token asset
let native_staking_token = AssetId {
inner: "KeqcLzNx9qSH5+lcJHBB9KNW+YPrBk5dKzvPMiypahA="
.to_string()
.into(),
alt_bech32m: "".to_string(),
alt_base_denom: "".to_string(),
};

let plan_js_value: JsValue = plan_transaction(
js_constants_params_value,
&planner_request.encode_to_vec(),
full_viewing_key.encode_to_vec().as_slice(),
native_staking_token.encode_to_vec().as_slice(),
)
.await
.unwrap();
Expand Down
Loading