Migrate to v3 explorer#248
Conversation
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.OpenSSF Scorecard
Scanned Files |
There was a problem hiding this comment.
Pull request overview
Migrates the Rootstock explorer integration from the legacy explorer API to the REST API v3 endpoints, updating request paths, response envelopes, and mapping utilities to keep the existing service-layer contracts working.
Changes:
- Switch
RSKExplorerAPImethods to v3 REST paths (/txs,/tokens,/events,/itxs,/balances) and add cursor/take handling. - Add v3 envelope types (
V3PaginatedResponse,V3SingleResponse,V3PaginationData) and mapping helpers for v3 payloads. - Update
.env.walletexplorer base URLs to/api/v3and adjust the RBTC balance tests for the new response shape.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/rskExplorerApi/index.ts |
Migrates explorer calls to v3 endpoints and introduces new pagination/cursor and mapping logic. |
src/rskExplorerApi/utils.ts |
Adds v3-to-legacy mapping helpers and pagination conversion utilities. |
src/rskExplorerApi/types.ts |
Introduces v3 response envelope/pagination TypeScript types. |
test/rskExplorerApi/index.test.ts |
Updates RBTC balance tests to match v3 response envelopes and request URLs. |
.env.wallet |
Updates explorer API base URLs to v3. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
TravellerOnTheRun
left a comment
There was a problem hiding this comment.
Code review — RIF Wallet Services (Explorer v3 migration)
Review feedback for this PR. Each item has a rationale and, where useful, proposed code you can copy into the branch.
1. Move shared transaction types out of the RSK explorer folder
Suggestion: Move types like IApiTransactions into a small shared module (for example src/types/transactions.ts) and import them from both rskExplorerApi and blockscoutApi.
Rationale: blockscoutApi currently imports types from rskExplorerApi. That couples two different backends to one module. Renaming or splitting the RSK explorer layer would force unrelated Blockscout edits. Shared DTOs should live in a neutral module, not under a single provider.
Approach: Multi-file change — one focused commit or one logical hunk per file works best.
Steps:
- Create
src/types/transactions.tsand moveIApiTransactions(and anything only used by that interface) out ofsrc/rskExplorerApi/types.ts. - In
src/rskExplorerApi/types.ts, either re-exportexport type { IApiTransactions } from '../types/transactions'or delete the local definition and import it where needed. - Update imports in
src/blockscoutApi/index.ts,src/repository/DataSource.ts, etc.
New file example (src/types/transactions.ts):
// src/types/transactions.ts
export interface IApiTransactions {
hash: string
nonce: number
blockHash: string
blockNumber: number
transactionIndex: number
from: string
to: string
gas: number
gasPrice: string
value: string
input: string
timestamp: number
receipt: unknown
txType: string
txId: string
}src/blockscoutApi/index.ts — use the shared import (and remove ../rskExplorerApi/types for this type):
import type { IApiTransactions } from '../types/transactions'2. Treat RBTC balance the same way as long token lists (pagination)
Suggestion: Either page through all balance rows the same way as tokens (fetchAllV3Rows), or confirm with the explorer API owners that the first page always contains the latest snapshot — then document that invariant in code.
Rationale: v2 returned the full balance history in one payload. v3 paginates. A single take-limited request can miss rows. Wrong RBTC balance is high-impact for wallets and for any service that calls this adapter. Prefer full pagination or an explicit, documented guarantee from the API.
Where it shows up today: src/rskExplorerApi/index.ts, method getRbtcBalanceByAddress — single axios.get with { take } only (no cursor loop).
Option A — Paginate like tokens (reuse fetchAllV3Rows, no dedupe):
Replace the whole getRbtcBalanceByAddress method in src/rskExplorerApi/index.ts with:
async getRbtcBalanceByAddress (address:string) {
const take = DEFAULT_TAKE
const path = `${this.url}/balances/address/${encodeURIComponent(address.toLowerCase())}`
return this.fetchAllV3Rows(path, take, () => null)
.then(rows => {
const typed = rows as Array<{ blockNumber: number, balance: string }>
if (typed.length === 0) return []
const lastBlock = typed.reduce((prev, current) =>
(prev.blockNumber > current.blockNumber) ? prev : current)
const weiHex = rbtcExplorerBalanceToHexWei(lastBlock.balance)
return [fromApiToRtbcBalance(weiHex, this.chainId)]
})
.catch(this.errorHandling)
}Option B — Keep one request but document the API rule:
Replace the opening of getRbtcBalanceByAddress (before the existing method body) with:
/**
* RBTC balance history is paginated on v3. We use one page here because <explain: e.g. API returns
* rows newest-first and the latest snapshot is always in the first page — confirm with explorer team>.
*/
async getRbtcBalanceByAddress (address:string) {3. Stop hard-coding event status as success
Suggestion: Map txStatus from the v3 payload when the field exists. If it does not exist, document that in a comment and use a value that does not imply a successful transaction.
Rationale: txStatus: '0x1' makes every event look successful. Failed transactions would still read as success for history UIs or any logic that branches on status. Prefer a real field from the API or a neutral / failure-safe default.
Where it shows up today: src/rskExplorerApi/utils.ts, fromV3ExplorerEventToIEvent.
Current code:
// src/rskExplorerApi/utils.ts — today
return {
// …
txStatus: '0x1'
}Replace fromV3ExplorerEventToIEvent in src/rskExplorerApi/utils.ts. Choose one approach based on the real v3 event JSON.
If the API exposes txStatus (string) on the event:
export function fromV3ExplorerEventToIEvent (e: {
event: string
blockNumber: number
timestamp: string
transactionHash: string
topic0?: string | null
topic1?: string | null
topic2?: string | null
topic3?: string | null
args?: Array<{ name: string, value: string }>
txStatus?: string | null
}): IEvent {
const topics = [e.topic0, e.topic1, e.topic2, e.topic3].filter(
(t): t is string => t != null && t !== ''
)
const args = (e.args ?? []).map(a => a.value)
return {
blockNumber: e.blockNumber,
event: e.event,
timestamp: parseInt(e.timestamp, 10),
topics,
args,
transactionHash: e.transactionHash,
txStatus: e.txStatus != null && e.txStatus !== '' ? e.txStatus : '0x0'
}
}If v3 has no status field yet, avoid defaulting to success — for example adjust only the txStatus line and add a short comment:
// v3 event payload has no tx status field — do not assume success
txStatus: '0x0'(Apply as part of a full function edit from export function through the closing }.)
4. Document cursor for v3 pagination
What cursor means here
Explorer v3 does not use numeric page indexes. It uses a cursor: an opaque value (string or number in JSON) meaning “continue from here.” The first request sends take (page size). The response includes paginationData.nextCursor / prevCursor. The next request sends that value as the query parameter cursor. This is standard cursor-style paging, not ?page=2.
Why add a short comment in code
Pagination depends on that query param. It is easy for a later change (refactor, quick cleanup, or someone grepping only for take) to assume cursor is unused or wrong and delete it. That would break token listing and transaction history paging. A one-line note next to the helper or the params object prevents that mistake. This is for future readers of this codebase, not for matching any external documentation site.
Which files use it
| Location | Role |
|---|---|
src/rskExplorerApi/index.ts |
fetchAllV3Rows — loops: sets params.cursor from paginationData.nextCursor for /tokens and /tokens/address/... |
src/rskExplorerApi/index.ts |
getTransactionsByAddress — maps client next / prev into params.cursor for /txs/address/... |
src/rskExplorerApi/utils.ts |
v3PaginationToPage — turns nextCursor / prevCursor into the Page object callers use |
Existing usage (excerpt) — src/rskExplorerApi/index.ts:
// Pagination loop for token endpoints: cursor comes from previous response
let cursor: string | undefined
for (let i = 0; i < MAX_V3_TOKEN_PAGES; i++) {
const params: { take: number; cursor?: string } = { take }
if (cursor) params.cursor = cursor
const response = await this.axios!.get(path, { params })
// …
cursor = String(p.nextCursor)
}// Transaction history: wallet passes our prev/next strings; we forward as cursor
const cursor = next ?? prev
const params: { take: number; cursor?: string } = { take }
if (cursor) params.cursor = cursor
return this.axios!.get(`${this.url}/txs/address/${encodeURIComponent(address)}`, { params })Proposed JSDoc on fetchAllV3Rows (src/rskExplorerApi/index.ts):
/**
* Fetches all pages for a v3 list endpoint, merging rows with optional dedupe by key.
* Uses `take` plus optional `cursor`; pass `paginationData.nextCursor` from the previous
* response as `cursor` on the next request until `hasMoreData` is false.
*/Proposed getTransactionsByAddress block (replace from const take = this.parseTake(limit) through if (cursor) params.cursor = cursor):
// Map wallet prev/next page tokens to explorer v3 query param `cursor` (see JSDoc on fetchAllV3Rows).
const take = this.parseTake(limit)
const path = `${this.url}/txs/address/${encodeURIComponent(address.toLowerCase())}`
const cursor = next ?? prev
const params: { take: number, cursor?: string } = { take }
if (cursor) params.cursor = cursorSummary: Add the JSDoc and/or the inline comment so cursor stays obvious as part of the paging contract for anyone editing this file later.
5. Make error handling easier to follow across methods
Suggestion: Use one clear contract per result shape — e.g. list-style methods return [] on failure via errorHandling, single-transaction fetch returns null — or document in two lines why two paths differ.
Rationale: Mixed empty shapes ([] vs null vs { data: [] }) are easy to get wrong on the next change. Callers should not have to reverse-engineer failure behavior from the implementation.
Code direction: Pick one documented contract, for example:
// List-style methods → never null; use [] on failure (keep errorHandling consistent)
// getTransaction-style → Promise<IApiTransactions | null> and null on failureProposed class JSDoc (insert immediately above export class RSKExplorerAPI extends DataSource {):
/**
* Explorer v3 adapter. Errors: list-style methods use `errorHandling` and return [];
* getTransaction returns null on failure; getTransactionsByAddress returns { prev, next, data: [] }.
*/
export class RSKExplorerAPI extends DataSource {6. Optional: smoke check against testnet explorer
Suggestion: Add a small integration-style test or script calling https://be.explorer.testnet.rootstock.io/api/v3 and asserting the envelope still has data and paginationData.
Rationale: Mocked unit tests match assumed JSON shapes only. The real service can drift from docs or from earlier assumptions. A thin, skippable network test catches that early.
New file (whole file — add via normal commit; optional describe.skip so CI stays offline):
// test/rskExplorerApi/explorer.v3.integration.test.ts
import axios from 'axios'
const TESTNET = 'https://be.explorer.testnet.rootstock.io/api/v3'
describe.skip('explorer v3 integration (network)', () => {
test('tokens envelope matches client expectations', async () => {
const { data } = await axios.get(`${TESTNET}/tokens`, { params: { take: 1 }, timeout: 15_000 })
expect(data).toHaveProperty('paginationData')
expect(data).toHaveProperty('data')
expect(Array.isArray(data.data)).toBe(true)
})
})Add as a new file or append the describe.skip suite to an existing test module; keep it skipped in CI if the pipeline must stay offline.
Closing
The v3 paths, response envelopes, and pagination approach look sound, and the added unit coverage for tokens and transactions is valuable. Resolving the points above would tighten boundaries between providers, reduce balance and status edge-case risk, and make the adapter safer for future callers. Happy to discuss any item in thread.
Migrating Explorer Backend from v2 to v3
V2: https://be.explorer.rootstock.io/doc/
V3: https://be.explorer.rootstock.io/api/v3/doc