diff --git a/.changeset/fuzzy-poems-train.md b/.changeset/fuzzy-poems-train.md new file mode 100644 index 0000000000..76743bf26d --- /dev/null +++ b/.changeset/fuzzy-poems-train.md @@ -0,0 +1,9 @@ +--- +'@penumbra-zone/storage': major +'@penumbra-zone/services': minor +'@penumbra-zone/query': minor +'@penumbra-zone/types': minor +'@penumbra-zone/wasm': minor +--- + +Modify GasPrices storage to support multi-asset fees diff --git a/packages/query/src/block-processor.ts b/packages/query/src/block-processor.ts index 4659d24207..b2dcdb225e 100644 --- a/packages/query/src/block-processor.ts +++ b/packages/query/src/block-processor.ts @@ -45,6 +45,7 @@ import { processActionDutchAuctionEnd } from './helpers/process-action-dutch-auc import { processActionDutchAuctionSchedule } from './helpers/process-action-dutch-auction-schedule'; import { processActionDutchAuctionWithdraw } from './helpers/process-action-dutch-auction-withdraw'; import { RootQuerier } from './root-querier'; +import { GasPrices } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/fee/v1/fee_pb'; declare global { // eslint-disable-next-line no-var @@ -162,12 +163,18 @@ export class BlockProcessor implements BlockProcessorInterface { await this.indexedDb.saveFmdParams(compactBlock.fmdParameters); } if (compactBlock.gasPrices) { - // TODO #1310 pre-populate assetId for native GasPrices using stakingTokenAssetId - await this.indexedDb.saveGasPrices(compactBlock.gasPrices); + await this.indexedDb.saveGasPrices( + new GasPrices({ + assetId: this.stakingAssetId, + ...compactBlock.gasPrices, + }), + ); + } + if (compactBlock.altGasPrices.length) { + for (const gasPrice of compactBlock.altGasPrices) { + await this.indexedDb.saveGasPrices(gasPrice); + } } - // if (compactBlock.altGasPrices) { - // TODO #1310 save altGasPrices to indexed-db - // } // wasm view server scan // - decrypts new notes diff --git a/packages/services/src/test-utils.ts b/packages/services/src/test-utils.ts index 6e4670f506..9ea7437946 100644 --- a/packages/services/src/test-utils.ts +++ b/packages/services/src/test-utils.ts @@ -9,7 +9,8 @@ export interface IndexedDbMock { constants?: Mock; getAppParams?: Mock; getAssetsMetadata?: Mock; - getGasPrices?: Mock; + getNativeGasPrices?: Mock; + getAltGasPrices?: Mock; getFmdParams?: Mock; getFullSyncHeight?: Mock; getNotesForVoting?: Mock; diff --git a/packages/services/src/view-service/gas-prices.test.ts b/packages/services/src/view-service/gas-prices.test.ts index d918b09b46..abe9ad62cc 100644 --- a/packages/services/src/view-service/gas-prices.test.ts +++ b/packages/services/src/view-service/gas-prices.test.ts @@ -20,7 +20,8 @@ describe('GasPrices request handler', () => { vi.resetAllMocks(); mockIndexedDb = { - getGasPrices: vi.fn(), + getNativeGasPrices: vi.fn(), + getAltGasPrices: vi.fn(), }; mockServices = { getWalletServices: vi.fn(() => @@ -40,7 +41,7 @@ describe('GasPrices request handler', () => { }); test('should successfully get gas prices when idb has them', async () => { - mockIndexedDb.getGasPrices?.mockResolvedValue(testData); + mockIndexedDb.getNativeGasPrices?.mockResolvedValue(testData); const gasPricesResponse = new GasPricesResponse( await gasPrices(new GasPricesRequest(), mockCtx), ); @@ -48,7 +49,7 @@ describe('GasPrices request handler', () => { }); test('should fail to get gas prices when idb has none', async () => { - mockIndexedDb.getGasPrices?.mockResolvedValue(undefined); + mockIndexedDb.getNativeGasPrices?.mockResolvedValue(undefined); await expect(gasPrices(new GasPricesRequest(), mockCtx)).rejects.toThrow( 'Gas prices is not available', ); diff --git a/packages/services/src/view-service/gas-prices.ts b/packages/services/src/view-service/gas-prices.ts index 1125a508db..454e8b78e4 100644 --- a/packages/services/src/view-service/gas-prices.ts +++ b/packages/services/src/view-service/gas-prices.ts @@ -23,11 +23,12 @@ import { Code, ConnectError } from '@connectrpc/connect'; export const gasPrices: Impl['gasPrices'] = async (_, ctx) => { const services = await ctx.values.get(servicesCtx)(); const { indexedDb } = await services.getWalletServices(); - const gasPrices = await indexedDb.getGasPrices(); + const gasPrices = await indexedDb.getNativeGasPrices(); + const altGasPRices = await indexedDb.getAltGasPrices(); if (!gasPrices) throw new ConnectError('Gas prices is not available', Code.NotFound); return { gasPrices, - // TODO #1310 add altGasPrices + altGasPRices, }; }; diff --git a/packages/services/src/view-service/transaction-planner/index.test.ts b/packages/services/src/view-service/transaction-planner/index.test.ts index b17bf773fc..6ecb9df69f 100644 --- a/packages/services/src/view-service/transaction-planner/index.test.ts +++ b/packages/services/src/view-service/transaction-planner/index.test.ts @@ -35,7 +35,7 @@ describe('TransactionPlanner request handler', () => { mockIndexedDb = { getFmdParams: vi.fn(), getAppParams: vi.fn(), - getGasPrices: vi.fn(), + getNativeGasPrices: vi.fn(), constants: vi.fn(), stakingTokenAssetId: vi.fn(), hasStakingAssetBalance: vi.fn(), @@ -92,7 +92,7 @@ describe('TransactionPlanner request handler', () => { }), }), ); - mockIndexedDb.getGasPrices?.mockResolvedValueOnce( + mockIndexedDb.getNativeGasPrices?.mockResolvedValueOnce( new GasPrices({ verificationPrice: 22n, executionPrice: 222n, diff --git a/packages/services/src/view-service/transaction-planner/index.ts b/packages/services/src/view-service/transaction-planner/index.ts index 627aaa8853..6cd09336a7 100644 --- a/packages/services/src/view-service/transaction-planner/index.ts +++ b/packages/services/src/view-service/transaction-planner/index.ts @@ -14,7 +14,7 @@ export const transactionPlanner: Impl['transactionPlanner'] = async (req, ctx) = const { indexedDb } = await services.getWalletServices(); // Query IndexedDB directly to check for the existence of staking token - const nativeToken = await indexedDb.hasStakingAssetBalance(); + const nativeToken = await indexedDb.hasStakingAssetBalance(req.source); // Initialize the gas fee token using the native staking token's asset ID // If there is no native token balance, extract and use an alternate gas fee token @@ -28,7 +28,7 @@ export const transactionPlanner: Impl['transactionPlanner'] = async (req, ctx) = if (!chainId) throw new ConnectError('ChainId not available', Code.FailedPrecondition); // Wasm planner needs the presence of gas prices in the db to work - const gasPrices = await indexedDb.getGasPrices(); + const gasPrices = await indexedDb.getNativeGasPrices(); if (!gasPrices) throw new ConnectError('Gas prices is not available', Code.FailedPrecondition); const idbConstants = indexedDb.constants(); diff --git a/packages/storage/src/indexed-db/config.ts b/packages/storage/src/indexed-db/config.ts index 8a028dd485..637aff2283 100644 --- a/packages/storage/src/indexed-db/config.ts +++ b/packages/storage/src/indexed-db/config.ts @@ -2,4 +2,4 @@ * The version number for the IndexedDB schema. This version number is used to manage * database upgrades and ensure that the correct schema version is applied. */ -export const IDB_VERSION = 43; +export const IDB_VERSION = 44; diff --git a/packages/storage/src/indexed-db/index.ts b/packages/storage/src/indexed-db/index.ts index 7cf7cd5db9..7240d67909 100644 --- a/packages/storage/src/indexed-db/index.ts +++ b/packages/storage/src/indexed-db/index.ts @@ -119,7 +119,7 @@ export class IndexedDb implements IndexedDbInterface { db.createObjectStore('SWAPS', { keyPath: 'swapCommitment.inner', }).createIndex('nullifier', 'nullifier.inner'); - db.createObjectStore('GAS_PRICES'); + db.createObjectStore('GAS_PRICES', { keyPath: 'assetId.inner' }); db.createObjectStore('POSITIONS', { keyPath: 'id.inner' }); db.createObjectStore('EPOCHS', { autoIncrement: true }); db.createObjectStore('VALIDATOR_INFOS'); @@ -385,21 +385,26 @@ export class IndexedDb implements IndexedDbInterface { return SwapRecord.fromJson(json); } - // TODO #1310 'getGasPrices()' should be renamed to 'getNativeGasPrice()' - async getGasPrices(): Promise<GasPrices | undefined> { - // TODO #1310 use this.stakingTokenAssetId as the key for the query - const jsonGasPrices = await this.db.get('GAS_PRICES', 'gas_prices'); + async getNativeGasPrices(): Promise<GasPrices | undefined> { + const jsonGasPrices = await this.db.get( + 'GAS_PRICES', + uint8ArrayToBase64(this.stakingTokenAssetId.inner), + ); if (!jsonGasPrices) return undefined; return GasPrices.fromJson(jsonGasPrices); } - // TODO #1310 implement getAltGasPrices() + async getAltGasPrices(): Promise<GasPrices[]> { + const allGasPrices = await this.db.getAll('GAS_PRICES'); + return allGasPrices + .map(gp => GasPrices.fromJson(gp)) + .filter(gp => !gp.assetId?.equals(this.stakingTokenAssetId)); + } async saveGasPrices(value: PartialMessage<GasPrices>): Promise<void> { await this.u.update({ table: 'GAS_PRICES', value: new GasPrices(value).toJson() as Jsonified<GasPrices>, - key: 'gas_prices', }); } @@ -815,7 +820,7 @@ export class IndexedDb implements IndexedDbInterface { }; } - async hasStakingAssetBalance(): Promise<boolean> { + async hasStakingAssetBalance(addressIndex: AddressIndex | undefined): Promise<boolean> { const spendableUMNotes = await this.db.getAllFromIndex( 'SPENDABLE_NOTES', 'assetId', @@ -824,7 +829,11 @@ export class IndexedDb implements IndexedDbInterface { return spendableUMNotes.some(note => { const umNote = SpendableNoteRecord.fromJson(note); - return umNote.heightSpent === 0n && !isZero(getAmountFromRecord(umNote)); + return ( + umNote.heightSpent === 0n && + !isZero(getAmountFromRecord(umNote)) && + umNote.addressIndex?.equals(addressIndex) + ); }); } } diff --git a/packages/storage/src/indexed-db/indexed-db.test.ts b/packages/storage/src/indexed-db/indexed-db.test.ts index e00dce25fe..4c0c2b0a0e 100644 --- a/packages/storage/src/indexed-db/indexed-db.test.ts +++ b/packages/storage/src/indexed-db/indexed-db.test.ts @@ -393,13 +393,14 @@ describe('IndexedDb', () => { const db = await IndexedDb.initialize({ ...generateInitialProps() }); const gasPrices = new GasPrices({ + assetId: db.stakingTokenAssetId, blockSpacePrice: 0n, compactBlockSpacePrice: 0n, verificationPrice: 0n, executionPrice: 0n, }); await db.saveGasPrices(gasPrices); - const savedPrices = await db.getGasPrices(); + const savedPrices = await db.getNativeGasPrices(); expect(gasPrices.equals(savedPrices)).toBeTruthy(); }); diff --git a/packages/types/src/indexed-db.ts b/packages/types/src/indexed-db.ts index a6d4af249f..d5ef7faa44 100644 --- a/packages/types/src/indexed-db.ts +++ b/packages/types/src/indexed-db.ts @@ -88,8 +88,8 @@ export interface IndexedDbInterface { getSwapByNullifier(nullifier: Nullifier): Promise<SwapRecord | undefined>; saveSwap(note: SwapRecord): Promise<void>; getSwapByCommitment(commitment: StateCommitment): Promise<SwapRecord | undefined>; - getGasPrices(): Promise<GasPrices | undefined>; - // TODO #1310 add getAltGasPrices() + getNativeGasPrices(): Promise<GasPrices | undefined>; + getAltGasPrices(): Promise<GasPrices[]>; saveGasPrices(value: PartialMessage<GasPrices>): Promise<void>; getNotesForVoting( addressIndex: AddressIndex | undefined, @@ -146,7 +146,7 @@ export interface IndexedDbInterface { auctionId: AuctionId, ): Promise<{ input: Value; output: Value } | undefined>; - hasStakingAssetBalance(): Promise<boolean>; + hasStakingAssetBalance(addressIndex: AddressIndex | undefined): Promise<boolean>; } export interface PenumbraDb extends DBSchema { @@ -231,9 +231,8 @@ export interface PenumbraDb extends DBSchema { nullifier: Jsonified<Required<SwapRecord>['nullifier']['inner']>; // base64 }; }; - // TODO #1310 use the assetId as key GAS_PRICES: { - key: 'gas_prices'; + key: Jsonified<Required<GasPrices>['assetId']['inner']>; // base64 value: Jsonified<GasPrices>; }; POSITIONS: { diff --git a/packages/wasm/crate/src/planner.rs b/packages/wasm/crate/src/planner.rs index fb35877de8..989fbcd1d4 100644 --- a/packages/wasm/crate/src/planner.rs +++ b/packages/wasm/crate/src/planner.rs @@ -1,13 +1,11 @@ -use crate::metadata::customize_symbol_inner; -use crate::note_record::SpendableNoteRecord; -use crate::storage::{IndexedDBStorage, OutstandingReserves}; -use crate::utils; -use crate::{error::WasmResult, swap_record::SwapRecord}; +use std::collections::BTreeMap; +use std::mem; + use anyhow::anyhow; use ark_ff::UniformRand; use decaf377::{Fq, Fr}; use penumbra_asset::asset::{Id, Metadata}; -use penumbra_asset::{Value, STAKING_TOKEN_ASSET_ID}; +use penumbra_asset::Value; use penumbra_auction::auction::dutch::actions::ActionDutchAuctionWithdrawPlan; use penumbra_auction::auction::dutch::{ ActionDutchAuctionEnd, ActionDutchAuctionSchedule, DutchAuctionDescription, @@ -38,11 +36,15 @@ use penumbra_transaction::ActionList; use penumbra_transaction::{plan::MemoPlan, ActionPlan, TransactionParameters}; use prost::Message; use rand_core::{OsRng, RngCore}; -use std::collections::BTreeMap; -use std::mem; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; +use crate::metadata::customize_symbol_inner; +use crate::note_record::SpendableNoteRecord; +use crate::storage::{IndexedDBStorage, OutstandingReserves}; +use crate::utils; +use crate::{error::WasmResult, swap_record::SwapRecord}; + /// Prioritize notes to spend to release value of a specific transaction. /// /// Various logic is possible for note selection. Currently, this method @@ -181,13 +183,15 @@ pub async fn plan_transaction( let chain_id: String = app_parameters.chain_id; + // Decode the gas fee token into an `Id` type + let fee_asset_id: Id = Id::decode(gas_fee_token)?; + // Request information about current gas prices - // TODO #1310 GasPrices record may not exist for alternative fee assets - let mut gas_prices: GasPrices = { + let gas_prices: GasPrices = { let gas_prices: penumbra_proto::core::component::fee::v1::GasPrices = serde_wasm_bindgen::from_value( storage - .get_gas_prices() + .get_gas_prices_by_asset_id(fee_asset_id) .await? .ok_or_else(|| anyhow!("GasPrices not available"))?, )?; @@ -202,28 +206,12 @@ 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 transaction_parameters = TransactionParameters { chain_id, expiry_height, ..Default::default() }; - transaction_parameters.fee.0.asset_id = alt_gas; + transaction_parameters.fee.0.asset_id = fee_asset_id; let mut actions_list = ActionList::default(); diff --git a/packages/wasm/crate/src/storage.rs b/packages/wasm/crate/src/storage.rs index 9c7b5b361f..eff821d981 100644 --- a/packages/wasm/crate/src/storage.rs +++ b/packages/wasm/crate/src/storage.rs @@ -323,14 +323,15 @@ impl IndexedDBStorage { .transpose()?) } - // TODO #1310 should be changed to get GasPrices by assetId - pub async fn get_gas_prices(&self) -> WasmResult<Option<JsValue>> { + pub async fn get_gas_prices_by_asset_id(&self, asset_id: Id) -> WasmResult<Option<JsValue>> { let tx = self .db .transaction_on_one(&self.constants.tables.gas_prices)?; let store = tx.object_store(&self.constants.tables.gas_prices)?; - Ok(store.get_owned("gas_prices")?.await?) + Ok(store + .get_owned(byte_array_to_base64(&asset_id.to_proto().inner))? + .await?) // TODO GasPrices is missing domain type impl, requiring this // .map(serde_wasm_bindgen::from_value) // .transpose()?) @@ -387,7 +388,7 @@ impl IndexedDBStorage { } } -fn byte_array_to_base64(byte_array: &Vec<u8>) -> String { +pub fn byte_array_to_base64(byte_array: &Vec<u8>) -> String { base64::Engine::encode(&base64::engine::general_purpose::STANDARD, byte_array) } diff --git a/packages/wasm/crate/tests/build.rs b/packages/wasm/crate/tests/build.rs index d436abd578..0db507f4ac 100644 --- a/packages/wasm/crate/tests/build.rs +++ b/packages/wasm/crate/tests/build.rs @@ -36,6 +36,7 @@ mod tests { use wasm_bindgen_test::*; use penumbra_wasm::planner::plan_transaction; + use penumbra_wasm::storage::byte_array_to_base64; use penumbra_wasm::{ build::build_action, keys::load_proving_key, @@ -372,7 +373,9 @@ mod tests { serde_wasm_bindgen::to_value(&"last_forgotten").unwrap(); let fmd_json_key: JsValue = serde_wasm_bindgen::to_value(&"params").unwrap(); let app_json_key: JsValue = serde_wasm_bindgen::to_value(&"params").unwrap(); - let gas_json_key: JsValue = serde_wasm_bindgen::to_value(&"gas_prices").unwrap(); + let gas_json_key: JsValue = JsValue::from_str(&byte_array_to_base64( + &STAKING_TOKEN_ASSET_ID.to_proto().inner, + )); store_note.put_val(&spendable_note_json).unwrap(); store_tree_commitments diff --git a/scripts/pack-public.sh b/scripts/pack-public.sh old mode 100644 new mode 100755