[BCN] Add MoralisAdapter and multiProvider error handling#4143
[BCN] Add MoralisAdapter and multiProvider error handling#4143leolambo wants to merge 5 commits intobitpay:masterfrom
Conversation
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).
There was a problem hiding this comment.
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 inAdapterFactory. - Extracts shared Moralis transform/query helpers into
moralis-utils.tsand reuses them inMoralisStateProvider. - Maps multi-provider errors to HTTP 503/400 in
txandaddressroutes; adds tests and a newredactUrlutility (+ 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.
| 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 }); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fair. Only 2 routes with 4 lines each for now, will centralize if more error codes are added.
There was a problem hiding this comment.
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);
});| 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***'); |
There was a problem hiding this comment.
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.
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.
kajoseph
left a comment
There was a problem hiding this comment.
Works well - nice work! Just had one comment/suggestion about the error handling
| 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 }); | ||
| } |
There was a problem hiding this comment.
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);
});
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 insideMoralisStateProvideris now in a sharedmoralis-utilsmodule so both the adapter and CSP use the same code.Changelog
MoralisStateProviderintomoralis-utils.ts(shared by adapter and CSP)MoralisAdapterimplementingIIndexedAPIAdapterwith error classification, health checks, and tx hash validationAdapterFactoryalongside AlchemyAllProvidersUnavailableErrornow returns 503 instead of 500 in tx and address routes;INVALID_REQUESTreturns 400redactUrlutility to strip API keys from logged URLsChecklist