Skip to content

Migrate to v3 explorer#248

Merged
Freshenext merged 4 commits into
developfrom
DAO-1985
Apr 29, 2026
Merged

Migrate to v3 explorer#248
Freshenext merged 4 commits into
developfrom
DAO-1985

Conversation

@sleyter93
Copy link
Copy Markdown
Collaborator

@sleyter93 sleyter93 commented Apr 8, 2026

@sleyter93 sleyter93 requested a review from Copilot April 8, 2026 14:32
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 8, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails

Scanned Files

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 RSKExplorerAPI methods 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.wallet explorer base URLs to /api/v3 and 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.

Comment thread src/rskExplorerApi/index.ts
Comment thread src/rskExplorerApi/index.ts Outdated
Comment thread src/rskExplorerApi/index.ts Outdated
Comment thread src/rskExplorerApi/index.ts
Copy link
Copy Markdown

@TravellerOnTheRun TravellerOnTheRun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Create src/types/transactions.ts and move IApiTransactions (and anything only used by that interface) out of src/rskExplorerApi/types.ts.
  2. In src/rskExplorerApi/types.ts, either re-export export type { IApiTransactions } from '../types/transactions' or delete the local definition and import it where needed.
  3. 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 = cursor

Summary: 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 failure

Proposed 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.

Comment thread src/blockscoutApi/index.ts
Comment thread src/rskExplorerApi/index.ts
@Freshenext Freshenext merged commit 1564b26 into develop Apr 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants