Skip to content

[BCN] Add MoralisAdapter and multiProvider error handling#4143

Open
leolambo wants to merge 5 commits intobitpay:masterfrom
leolambo:multiProviderMoralisSupport
Open

[BCN] Add MoralisAdapter and multiProvider error handling#4143
leolambo wants to merge 5 commits intobitpay:masterfrom
leolambo:multiProviderMoralisSupport

Conversation

@leolambo
Copy link
Copy Markdown
Contributor

@leolambo leolambo commented Apr 16, 2026

Description

The multi-provider orchestrator already supported Alchemy, but Moralis was missing an adapter. This adds MoralisAdapter, registers it in the factory, and maps provider-level errors to proper HTTP status codes in the API routes. The transformation logic that was duplicated inside MoralisStateProvider is now in a shared moralis-utils module so both the adapter and CSP use the same code.

Changelog

  • Extracted transformation and query-building functions from MoralisStateProvider into moralis-utils.ts (shared by adapter and CSP)
  • Added MoralisAdapter implementing IIndexedAPIAdapter with error classification, health checks, and tx hash validation
  • Registered Moralis in AdapterFactory alongside Alchemy
  • AllProvidersUnavailableError now returns 503 instead of 500 in tx and address routes; INVALID_REQUEST returns 400
  • Added redactUrl utility to strip API keys from logged URLs
  • Integration tests for both adapters (gated behind env vars, skipped without keys)

Checklist

  • I have read CONTRIBUTING.md and verified that this PR follows the guidelines and requirements outlined in it.

Move transformation and query-building logic out of
MoralisStateProvider into a shared moralis-utils module.
Both the upcoming MoralisAdapter and the existing CSP
now import from the same source.

Also add a general-purpose redactUrl utility to prevent
API key leakage in log output.
Implement IIndexedAPIAdapter for Moralis REST API with
error classification, input validation, and health checks.
Register in AdapterFactory alongside Alchemy so the
multi-provider orchestrator can failover between them.
AllProvidersUnavailableError now returns 503 instead of 500
so clients can distinguish between server bugs and temporary
provider outages. INVALID_REQUEST AdapterErrors return 400.
Integration tests verify both adapters against live APIs
(gated behind env vars, skipped in CI without keys).
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

Adds Moralis as a first-class indexed API provider in bitcore-node’s multi-provider chain-state layer, and improves API route behavior when upstream indexed providers fail.

Changes:

  • Introduces MoralisAdapter (IIndexedAPIAdapter) and registers it in AdapterFactory.
  • Extracts shared Moralis transform/query helpers into moralis-utils.ts and reuses them in MoralisStateProvider.
  • Maps multi-provider errors to HTTP 503/400 in tx and address routes; adds tests and a new redactUrl utility (+ tests).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
packages/bitcore-node/src/providers/chain-state/external/adapters/moralis.ts New Moralis adapter: tx lookup, streaming, health check, error classification
packages/bitcore-node/src/providers/chain-state/external/adapters/moralis-utils.ts Shared Moralis query/transform utilities for adapter + CSP
packages/bitcore-node/src/providers/chain-state/external/adapters/factory.ts Registers moralis in adapter registry
packages/bitcore-node/src/modules/moralis/api/csp.ts Replaces duplicated Moralis logic with shared utilities
packages/bitcore-node/src/routes/api/tx.ts Maps provider errors to 503/400 responses across tx endpoints
packages/bitcore-node/src/routes/api/address.ts Maps provider errors to 503/400 responses for address endpoints
packages/bitcore-node/src/utils/redactUrl.ts Adds URL API-key redaction helper
packages/bitcore-node/test/unit/utils/index.test.ts Adds unit tests for redactUrl
packages/bitcore-node/test/unit/adapters/moralis.test.ts Adds unit tests for Moralis adapter + shared utils
packages/bitcore-node/test/unit/adapters/factory.test.ts Updates supported provider assertions (adds moralis)
packages/bitcore-node/test/integration/routes/tx.test.ts Adds integration coverage for tx-route error→HTTP mapping
packages/bitcore-node/test/integration/multiProvider/csp.test.ts Adds env-gated integration checks for Moralis + Alchemy adapters

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/bitcore-node/src/providers/chain-state/external/adapters/moralis.ts Outdated
Comment thread packages/bitcore-node/src/routes/api/tx.ts
Comment on lines +42 to +47
if (err instanceof AllProvidersUnavailableError) {
return res.status(503).json({ error: 'All indexed API providers unavailable', message: err.message });
}
if (err instanceof AdapterError && err.code === AdapterErrorCode.INVALID_REQUEST) {
return res.status(400).json({ error: 'Invalid request', message: err.message });
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

The same error→HTTP mapping block is duplicated across multiple handlers here (and similarly in address.ts). To avoid drift as more AdapterErrorCodes are mapped, consider centralizing this in a small helper (e.g., handleIndexedProviderError(res, err)) or Express error middleware.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

@leolambo leolambo Apr 17, 2026

Choose a reason for hiding this comment

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

Fair. Only 2 routes with 4 lines each for now, will centralize if more error codes are added.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm counting 7 routes. I think it may be worth defining and using a universal API error response handling fn like so:

// src/routes/apiUtils.ts

export function respondWithError(res, err) {
    if (err instanceof AllProvidersUnavailableError) {
      return res.status(503).json({ error: 'All indexed API providers unavailable', message: err.message });
    }
    if (err instanceof AdapterError && err.code === AdapterErrorCode.INVALID_REQUEST) {
      return res.status(400).json({ error: 'Invalid request', message: err.message });
    }
    return res.status(500).send(err.message || err);
}

then implementing like

router.get('/my/path', async function (req: Request, res) {
  try {
    // ...
  } catch (err: any) {
    logger.error('Error getting address balance: %o', err.stack || err.message || err);
    apiUtils.respondWithError(res, err);
});

Comment thread packages/bitcore-node/src/modules/moralis/api/csp.ts
Comment on lines +1 to +4
export function redactUrl(url: string): string {
return url
.replace(/\/(v[23])\/[a-zA-Z0-9_-]+/g, '/$1/***REDACTED***')
.replace(/([?&])(apikey|api_key|key)=[^&]+/gi, '$1$2=***REDACTED***');
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

The PR description mentions redacting API keys from logged URLs, but redactUrl is not used anywhere in the codebase yet (only defined/tests). A high-impact place to apply it is where axios errors capture err.config.url for logging (e.g., ExternalApiStream.onStream), since provider URLs can embed API keys.

Copilot uses AI. Check for mistakes.
Comment thread packages/bitcore-node/src/modules/moralis/api/csp.ts Outdated
Comment thread packages/bitcore-node/src/modules/moralis/api/csp.ts
The order param from transformMoralisQueryParams (derived from
direction) was always overwritten by `|| 'DESC'` because args.order
is typically undefined. Changed to `?? query.order ?? 'DESC'`.

_calculateConfirmations was called with the raw Moralis tx (has
block_number) instead of the transformed _tx (has blockHeight),
so confirmations were always 0. Also added NaN guard in the
adapter for pending txs without a block number.
Copy link
Copy Markdown
Collaborator

@kajoseph kajoseph left a comment

Choose a reason for hiding this comment

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

Works well - nice work! Just had one comment/suggestion about the error handling

Comment on lines +42 to +47
if (err instanceof AllProvidersUnavailableError) {
return res.status(503).json({ error: 'All indexed API providers unavailable', message: err.message });
}
if (err instanceof AdapterError && err.code === AdapterErrorCode.INVALID_REQUEST) {
return res.status(400).json({ error: 'Invalid request', message: err.message });
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm counting 7 routes. I think it may be worth defining and using a universal API error response handling fn like so:

// src/routes/apiUtils.ts

export function respondWithError(res, err) {
    if (err instanceof AllProvidersUnavailableError) {
      return res.status(503).json({ error: 'All indexed API providers unavailable', message: err.message });
    }
    if (err instanceof AdapterError && err.code === AdapterErrorCode.INVALID_REQUEST) {
      return res.status(400).json({ error: 'Invalid request', message: err.message });
    }
    return res.status(500).send(err.message || err);
}

then implementing like

router.get('/my/path', async function (req: Request, res) {
  try {
    // ...
  } catch (err: any) {
    logger.error('Error getting address balance: %o', err.stack || err.message || err);
    apiUtils.respondWithError(res, err);
});

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.

3 participants