Skip to content
Open
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
109 changes: 109 additions & 0 deletions packages/sdk/NEXT_EPOCH_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -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
92 changes: 91 additions & 1 deletion packages/sdk/src/rpc/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import {

import {
Balance,
BlockHeader,
BondsResponse,
DelegationTotals,
DelegatorsVotes,
GasCosts,
MaspTokenRewards,
NextEpochInfo,
StakingPositions,
StakingTotals,
StakingTotalsResponse,
Expand All @@ -37,7 +39,7 @@ export class Rpc {
constructor(
protected readonly sdk: SdkWasm,
protected readonly query: QueryWasm
) {}
) { }

/**
* Query balances from chain
Expand Down Expand Up @@ -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<BlockHeader> {
// 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<NextEpochInfo> {
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
Expand Down
21 changes: 21 additions & 0 deletions packages/sdk/src/rpc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
67 changes: 67 additions & 0 deletions packages/shared/lib/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,73 @@ 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<JsValue, JsError> {
// 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 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."))

// 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<u64>) -> Result<JsValue, JsError> {
// 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
Expand Down
24 changes: 13 additions & 11 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,19 @@ const promiseWithTimeout =
fn: (...args: U) => Promise<T>,
opts?: TimeoutOpts
) =>
(...args: U): Promise<T> => {
const { timeout, error } = { ...DEFAULT_OPTS, ...opts };
(...args: U): Promise<T> => {
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 {
Expand All @@ -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";
Expand Down
Loading