From 9bbbd160aabfedf709ee51edb76707377ca203ee Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 24 Jul 2025 21:39:33 -0700 Subject: [PATCH 1/2] feat: next epoch info WIP --- packages/sdk/NEXT_EPOCH_IMPLEMENTATION.md | 109 ++++++++++++++++++++++ packages/sdk/src/rpc/rpc.ts | 92 +++++++++++++++++- packages/sdk/src/rpc/types.ts | 21 +++++ packages/shared/lib/src/query.rs | 53 +++++++++++ packages/shared/src/index.ts | 24 ++--- 5 files changed, 287 insertions(+), 12 deletions(-) create mode 100644 packages/sdk/NEXT_EPOCH_IMPLEMENTATION.md diff --git a/packages/sdk/NEXT_EPOCH_IMPLEMENTATION.md b/packages/sdk/NEXT_EPOCH_IMPLEMENTATION.md new file mode 100644 index 000000000..f2faa678a --- /dev/null +++ b/packages/sdk/NEXT_EPOCH_IMPLEMENTATION.md @@ -0,0 +1,109 @@ +# Next Epoch Implementation + +## Overview +This document describes the implementation of the `getNextEpoch` function in the Namada JavaScript SDK, which provides information about the next epoch including timing and block height details. + +## Usage + +```typescript +import { Sdk } from "@namada/sdk/web"; +import { NextEpochInfo } from "@namada/sdk"; + +// Initialize SDK +const sdk = new Sdk(/* ... */); + +// Get next epoch information +const nextEpochInfo: NextEpochInfo = await sdk.rpc.getNextEpoch(); + +console.log(`Next epoch: ${nextEpochInfo.nextEpoch}`); +console.log(`Min block height: ${nextEpochInfo.minBlockHeight}`); +console.log(`Time until next epoch: ${nextEpochInfo.minTimeUntilNextEpoch} seconds`); +``` + +## Return Type + +```typescript +type NextEpochInfo = { + nextEpoch: bigint; // The next epoch number + minBlockHeight: number; // Minimum block height for the next epoch + minTimeUntilNextEpoch: number; // Time in seconds until the next epoch +}; +``` + +## Implementation Status + +### ✅ Completed (JavaScript/TypeScript Layer) +- [x] TypeScript type definitions (`NextEpochInfo`, `BlockHeader`) +- [x] High-level SDK implementation in `rpc.ts` +- [x] Error handling and data transformation +- [x] Import/export statements +- [x] Type safety and runtime method existence checks +- [x] Graceful error messages when Rust methods are not yet implemented +- [x] Build verification - TypeScript compilation passes + +### ⏳ Pending (Rust WASM Layer) +The following Rust methods need to be implemented in the Namada SDK's WASM layer: + +#### `query_next_epoch_info()` +**Location:** Should be added to the `Query` struct in the Rust WASM bindings + +**Expected Return Format:** +```rust +// Expected JSON structure from Rust +{ + "next_epoch": u64, + "min_block_height": u64, + "next_epoch_time": "RFC3339 timestamp string", + // ... other relevant fields +} +``` + +**Rust Implementation Notes:** +- Should call `rpc::query_next_epoch_info` from the Rust Namada SDK +- Return serialized JSON that matches the expected format above +- Handle RPC errors appropriately + +#### `query_block_header(height?: number)` +**Location:** Should be added to the `Query` struct in the Rust WASM bindings + +**Expected Return Format:** +```rust +// Expected JSON structure from Rust +{ + "height": u64, + "time": "RFC3339 timestamp string", + "hash": "block hash string", + "proposer_address": "validator address string" +} +``` + +**Rust Implementation Notes:** +- Should call `rpc::query_block_header` from the Rust Namada SDK +- If `height` is `null`, query the latest block +- Return serialized JSON that matches the expected format above + +## Integration Points + +### JavaScript SDK Files Modified +1. `packages/sdk/src/rpc/types.ts` - Added type definitions +2. `packages/sdk/src/rpc/rpc.ts` - Added main implementation + +### Rust Files That Need Updates +1. **WASM Query Struct** - Add the two new query methods +2. **RPC Integration** - Connect to the actual Rust SDK RPC calls +3. **Serialization** - Ensure proper JSON serialization of return types + +## Error Handling + +The implementation includes comprehensive error handling: +- Network/RPC failures are caught and re-thrown with descriptive messages +- Invalid timestamps are handled gracefully +- Negative time calculations are clamped to 0 + +## Testing Recommendations + +Once the Rust implementation is complete, test: +1. **Basic functionality** - Verify correct epoch and timing information +2. **Edge cases** - Test around epoch boundaries +3. **Error scenarios** - Test network failures and invalid responses +4. **Performance** - Verify the combined RPC calls don't cause significant delays \ No newline at end of file diff --git a/packages/sdk/src/rpc/rpc.ts b/packages/sdk/src/rpc/rpc.ts index 779644243..0d5bcf01f 100644 --- a/packages/sdk/src/rpc/rpc.ts +++ b/packages/sdk/src/rpc/rpc.ts @@ -14,11 +14,13 @@ import { import { Balance, + BlockHeader, BondsResponse, DelegationTotals, DelegatorsVotes, GasCosts, MaspTokenRewards, + NextEpochInfo, StakingPositions, StakingTotals, StakingTotalsResponse, @@ -37,7 +39,7 @@ export class Rpc { constructor( protected readonly sdk: SdkWasm, protected readonly query: QueryWasm - ) {} + ) { } /** * Query balances from chain @@ -219,6 +221,94 @@ export class Rpc { return checksums; } + /** + * Query block header information + * @async + * @param height - Block height to query (optional, defaults to latest) + * @returns Block header information + * @throws Error when Rust WASM method is not yet implemented + */ + async queryBlockHeader(height?: number): Promise { + // Check if the method exists (will be available once Rust implementation is added) + if (!(this.query as any).query_block_header) { + throw new Error("query_block_header is not yet implemented in the Rust WASM layer"); + } + + const blockHeader = await (this.query as any).query_block_header(height || null) as any; + + // Type-safe parsing of the response + if (!blockHeader || typeof blockHeader !== 'object') { + throw new Error("Invalid block header response from Rust SDK"); + } + + return { + height: Number(blockHeader.height), + time: String(blockHeader.time), + hash: String(blockHeader.hash), + proposerAddress: String(blockHeader.proposer_address), + }; + } + + /** + * Get next epoch information including epoch number, min block height, and time until next epoch + * @async + * @returns Next epoch information with timing details + * @throws Error when Rust WASM methods are not yet implemented + */ + async getNextEpoch(): Promise { + try { + // Check if the methods exist (will be available once Rust implementation is added) + if (!(this.query as any).query_next_epoch_info) { + throw new Error("query_next_epoch_info is not yet implemented in the Rust WASM layer"); + } + if (!(this.query as any).query_block_header) { + throw new Error("query_block_header is not yet implemented in the Rust WASM layer"); + } + + // Get next epoch info from Rust SDK + const nextEpochInfo = await (this.query as any).query_next_epoch_info() as any; + + // Get current block header to calculate timing + const currentBlockHeader = await (this.query as any).query_block_header(null) as any; + + // Type-safe parsing of the responses + if (!nextEpochInfo || typeof nextEpochInfo !== 'object') { + throw new Error("Invalid next epoch info response from Rust SDK"); + } + if (!currentBlockHeader || typeof currentBlockHeader !== 'object') { + throw new Error("Invalid block header response from Rust SDK"); + } + + // Parse the response (structure will depend on Rust implementation) + const nextEpoch = BigInt(nextEpochInfo.next_epoch); + const minBlockHeight = Number(nextEpochInfo.min_block_height); + const nextEpochTime = new Date(nextEpochInfo.next_epoch_time); + const currentTime = new Date(currentBlockHeader.time); + + // Validate parsed data + if (isNaN(minBlockHeight)) { + throw new Error("Invalid min_block_height in next epoch info"); + } + if (isNaN(nextEpochTime.getTime())) { + throw new Error("Invalid next_epoch_time in next epoch info"); + } + if (isNaN(currentTime.getTime())) { + throw new Error("Invalid time in current block header"); + } + + // Calculate time until next epoch in seconds + const minTimeUntilNextEpoch = Math.max(0, Math.floor((nextEpochTime.getTime() - currentTime.getTime()) / 1000)); + + return { + nextEpoch, + minBlockHeight, + minTimeUntilNextEpoch, + }; + } catch (error) { + throw new Error(`Failed to get next epoch information: ${error}`); + } + } + /** * Broadcast a Tx to the ledger * @async diff --git a/packages/sdk/src/rpc/types.ts b/packages/sdk/src/rpc/types.ts index f201b68ab..eac217898 100644 --- a/packages/sdk/src/rpc/types.ts +++ b/packages/sdk/src/rpc/types.ts @@ -84,3 +84,24 @@ export type WasmHash = { path: string; hash: string; }; + + + +/** + * Next epoch information + */ +export type NextEpochInfo = { + nextEpoch: bigint; + minBlockHeight: number; + minTimeUntilNextEpoch: number; // in seconds +}; + +/** + * Block header information + */ +export type BlockHeader = { + height: number; + time: string; + hash: string; + proposerAddress: string; +}; diff --git a/packages/shared/lib/src/query.rs b/packages/shared/lib/src/query.rs index 485baeeb4..3445f81e7 100644 --- a/packages/shared/lib/src/query.rs +++ b/packages/shared/lib/src/query.rs @@ -802,6 +802,59 @@ impl Query { None } } + + /// Gets next epoch information including timing and block details + /// + /// # Errors + /// + /// Returns an error if the RPC call fails + pub async fn query_next_epoch_info(&self) -> Result { + // TODO: This needs to be implemented in the Namada SDK RPC module + // For now, we'll return a placeholder structure + + // The actual implementation should look something like: + // let next_epoch_info = rpc::query_next_epoch_info(&self.client).await?; + + // For now, return an error indicating this needs to be implemented + Err(JsError::new("query_next_epoch_info is not yet implemented in the Namada SDK. Please implement rpc::query_next_epoch_info first.")) + + // Expected return structure when implemented: + // let result = json!({ + // "next_epoch": next_epoch_info.next_epoch, + // "min_block_height": next_epoch_info.min_block_height, + // "next_epoch_time": next_epoch_info.next_epoch_time.to_rfc3339(), + // }); + // to_js_result(result) + } + + /// Gets block header information for a specific height + /// + /// # Arguments + /// + /// * `height` - Block height to query (optional, defaults to latest) + /// + /// # Errors + /// + /// Returns an error if the RPC call fails + pub async fn query_block_header(&self, height: Option) -> Result { + // TODO: This needs to be implemented in the Namada SDK RPC module + // For now, we'll return a placeholder structure + + // The actual implementation should look something like: + // let block_header = rpc::query_block_header(&self.client, height).await?; + + // For now, return an error indicating this needs to be implemented + Err(JsError::new("query_block_header is not yet implemented in the Namada SDK. Please implement rpc::query_block_header first.")) + + // Expected return structure when implemented: + // let result = json!({ + // "height": block_header.height, + // "time": block_header.time.to_rfc3339(), + // "hash": block_header.hash.to_string(), + // "proposer_address": block_header.proposer_address.to_string(), + // }); + // to_js_result(result) + } } //TODO: remove after moving this fn from apps to shared diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 91f8c899c..b7899a901 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -25,19 +25,19 @@ const promiseWithTimeout = fn: (...args: U) => Promise, opts?: TimeoutOpts ) => - (...args: U): Promise => { - const { timeout, error } = { ...DEFAULT_OPTS, ...opts }; + (...args: U): Promise => { + const { timeout, error } = { ...DEFAULT_OPTS, ...opts }; - return new Promise(async (resolve, reject) => { - const t = setTimeout(() => { - reject(error(timeout)); - }, timeout); + return new Promise(async (resolve, reject) => { + const t = setTimeout(() => { + reject(error(timeout)); + }, timeout); - const res = await fn(...args); - clearTimeout(t); - resolve(res); - }); - }; + const res = await fn(...args); + clearTimeout(t); + resolve(res); + }); + }; //Fallbacks for rust panics export class Query extends RustQuery { @@ -54,6 +54,8 @@ export class Query extends RustQuery { get_total_delegations = promiseWithTimeout( super.get_total_delegations.bind(this) ); + query_next_epoch_info = promiseWithTimeout(super.query_next_epoch_info.bind(this)); + query_block_header = promiseWithTimeout(super.query_block_header.bind(this)); } export * from "./types"; From 8984644e6466cf7a41bef70e38781be8af3ea44d Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 24 Jul 2025 22:13:26 -0700 Subject: [PATCH 2/2] feat: wip 2 --- packages/shared/lib/src/query.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/shared/lib/src/query.rs b/packages/shared/lib/src/query.rs index 3445f81e7..dba0719c4 100644 --- a/packages/shared/lib/src/query.rs +++ b/packages/shared/lib/src/query.rs @@ -813,10 +813,24 @@ impl Query { // For now, we'll return a placeholder structure // The actual implementation should look something like: - // let next_epoch_info = rpc::query_next_epoch_info(&self.client).await?; + let current_epoch = query_epoch(&self.client).await?; + let (this_epoch_first_height, epoch_duration) = + rpc::query_next_epoch_info(&self.client).await?; + let this_epoch_first_height_header = + rpc::query_block_header(&self.client, this_epoch_first_height) + .await? + .unwrap(); + + let first_block_time = this_epoch_first_height_header.time; + let next_epoch_time = first_block_time + epoch_duration.min_duration; + let next_epoch_block = this_epoch_first_height.0 + epoch_duration.min_num_of_blocks; + let next_epoch = current_epoch.next(); + + let result: (Epoch, u64, String) = + (next_epoch, next_epoch_block, next_epoch_time.to_rfc3339()); // For now, return an error indicating this needs to be implemented - Err(JsError::new("query_next_epoch_info is not yet implemented in the Namada SDK. Please implement rpc::query_next_epoch_info first.")) + // Err(JsError::new("query_next_epoch_info is not yet implemented in the Namada SDK. Please implement rpc::query_next_epoch_info first.")) // Expected return structure when implemented: // let result = json!({ @@ -824,7 +838,7 @@ impl Query { // "min_block_height": next_epoch_info.min_block_height, // "next_epoch_time": next_epoch_info.next_epoch_time.to_rfc3339(), // }); - // to_js_result(result) + to_js_result(result) } /// Gets block header information for a specific height