holdge-rpc 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/chains.d.ts +16 -0
- package/dist/chains.js +23 -0
- package/dist/failover.d.ts +9 -0
- package/dist/failover.js +77 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +45 -0
- package/dist/rabby.d.ts +7 -0
- package/dist/rabby.js +70 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @holdge/rpc
|
|
2
|
+
|
|
3
|
+
Provider RPC **resiliente** pras apps Holdge (comcripto, grouppay, swap.holdge, agent.holdge).
|
|
4
|
+
Uma camada só, em vez de cada app reimplementar fallback de RPC.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
HTTP → Rabby (grátis) → públicos (grátis) → Alchemy (pago, último recurso) (FailoverProvider estrito)
|
|
8
|
+
WSS → Alchemy (se key) → público (Rabby não tem WebSocket)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- **Ordenado por CUSTO:** Rabby e públicos são grátis e vêm primeiro; o **Alchemy (pago) só é tocado
|
|
12
|
+
quando TODOS os grátis falham**. Isso evita queimar CU à toa.
|
|
13
|
+
- **Failover ESTRITO por-erro** (`FailoverProvider`, não o `FallbackProvider` do ethers). O FallbackProvider
|
|
14
|
+
é latência-based: ele raceia e prefere o mais *rápido* (Alchemy direto), queimando CU. O nosso só cai pro
|
|
15
|
+
próximo quando o atual **lança** (erro de transporte). Todas as ops passam por `_perform` → cobre o caminho
|
|
16
|
+
inteiro (getLogs, getBalance, call, estimateGas, broadcastTransaction, getTransactionCount…).
|
|
17
|
+
|
|
18
|
+
## ⚠️ Rabby exige `node:https` (NÃO `fetch`)
|
|
19
|
+
|
|
20
|
+
A `api.rabby.io` (server `dbkserver` atrás de CloudFront) **bloqueia (403) o `fetch` do Node (undici)** por
|
|
21
|
+
fingerprint TLS/HTTP — **independe de User-Agent/headers**. O módulo **`https` nativo do Node passa (200)**.
|
|
22
|
+
Verificado empiricamente: `curl` e `https` nativo → 200+JSON; `fetch`/undici → 403 página de bloqueio.
|
|
23
|
+
|
|
24
|
+
Por isso o `RabbyProvider` usa `node:https` (com keep-alive), e **a lib é server-side (Node) apenas** — não
|
|
25
|
+
roda em browser/edge runtime. (O GroupPay usa `fetch` e por isso está com Rabby OFFLINE; esta lib o supera.)
|
|
26
|
+
|
|
27
|
+
> Nota: BSC no proxy Rabby costuma dar **429** (rate-limit) sob carga — o failover cobre caindo pro público.
|
|
28
|
+
|
|
29
|
+
## Uso
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { httpProvider, wssProvider } from "@holdge/rpc";
|
|
33
|
+
|
|
34
|
+
const http = httpProvider("polygon", { alchemyKey: process.env.ALCHEMY_API_KEY });
|
|
35
|
+
await http.getBalance("0x..."); // Rabby → públicos → Alchemy (pago só em último caso)
|
|
36
|
+
|
|
37
|
+
const ws = wssProvider("polygon", { alchemyKey: process.env.ALCHEMY_API_KEY });
|
|
38
|
+
ws.on("block", (n) => console.log("novo bloco", n)); // Alchemy/público (Rabby não faz WSS)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Sem `alchemyKey` → roda **Rabby → públicos** ($0, sem provedor pago). `useRabby: false` desliga a Rabby.
|
|
42
|
+
|
|
43
|
+
## Redes
|
|
44
|
+
ethereum · polygon · base · arbitrum · bsc (extensível em `src/chains.ts` — Rabby cobre ~103 EVMs).
|
|
45
|
+
|
|
46
|
+
## Instalação (consumidor)
|
|
47
|
+
Use `--install-links` pra copiar (deduplica o `ethers` via peer dep, evita tipos duplicados):
|
|
48
|
+
```bash
|
|
49
|
+
npm install file:../holdge-rpc --install-links
|
|
50
|
+
```
|
package/dist/chains.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config das redes EVM suportadas. Rabby cobre ~103 EVMs via proxy; aqui ficam as que as apps Holdge usam.
|
|
3
|
+
* Pra adicionar rede: descubra o `rabbyId` (chain_id da Rabby, ex.: eth/bsc/matic/arb/base/op/avax) e o
|
|
4
|
+
* subdomínio Alchemy + RPC/WSS público. `publicHttp` é o último fallback; `publicWss` cobre o que a Rabby não faz.
|
|
5
|
+
*/
|
|
6
|
+
export type HoldgeChainKey = "ethereum" | "polygon" | "base" | "arbitrum" | "bsc";
|
|
7
|
+
export type HoldgeChain = {
|
|
8
|
+
key: HoldgeChainKey;
|
|
9
|
+
chainId: number;
|
|
10
|
+
rabbyId: string;
|
|
11
|
+
alchemyNet: string;
|
|
12
|
+
publicHttp: string[];
|
|
13
|
+
publicWss: string;
|
|
14
|
+
};
|
|
15
|
+
export declare const HOLDGE_CHAINS: Record<HoldgeChainKey, HoldgeChain>;
|
|
16
|
+
export declare const HOLDGE_CHAIN_KEYS: HoldgeChainKey[];
|
package/dist/chains.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const HOLDGE_CHAINS = {
|
|
2
|
+
ethereum: {
|
|
3
|
+
key: "ethereum", chainId: 1, rabbyId: "eth", alchemyNet: "eth-mainnet",
|
|
4
|
+
publicHttp: ["https://ethereum-rpc.publicnode.com"], publicWss: "wss://ethereum-rpc.publicnode.com",
|
|
5
|
+
},
|
|
6
|
+
polygon: {
|
|
7
|
+
key: "polygon", chainId: 137, rabbyId: "matic", alchemyNet: "polygon-mainnet",
|
|
8
|
+
publicHttp: ["https://polygon-bor-rpc.publicnode.com", "https://polygon-rpc.com"], publicWss: "wss://polygon-bor-rpc.publicnode.com",
|
|
9
|
+
},
|
|
10
|
+
base: {
|
|
11
|
+
key: "base", chainId: 8453, rabbyId: "base", alchemyNet: "base-mainnet",
|
|
12
|
+
publicHttp: ["https://base-rpc.publicnode.com", "https://mainnet.base.org"], publicWss: "wss://base-rpc.publicnode.com",
|
|
13
|
+
},
|
|
14
|
+
arbitrum: {
|
|
15
|
+
key: "arbitrum", chainId: 42161, rabbyId: "arb", alchemyNet: "arb-mainnet",
|
|
16
|
+
publicHttp: ["https://arbitrum-one-rpc.publicnode.com", "https://arb1.arbitrum.io/rpc"], publicWss: "wss://arbitrum-one-rpc.publicnode.com",
|
|
17
|
+
},
|
|
18
|
+
bsc: {
|
|
19
|
+
key: "bsc", chainId: 56, rabbyId: "bsc", alchemyNet: "bnb-mainnet",
|
|
20
|
+
publicHttp: ["https://bsc-rpc.publicnode.com", "https://bsc-dataseed.binance.org"], publicWss: "wss://bsc-rpc.publicnode.com",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
export const HOLDGE_CHAIN_KEYS = Object.keys(HOLDGE_CHAINS);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AbstractProvider, Network } from "ethers";
|
|
2
|
+
import type { PerformActionRequest } from "ethers";
|
|
3
|
+
export declare class FailoverProvider extends AbstractProvider {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(subs: AbstractProvider[], network: Network);
|
|
6
|
+
_detectNetwork(): Promise<Network>;
|
|
7
|
+
_perform<T = unknown>(req: PerformActionRequest): Promise<T>;
|
|
8
|
+
destroy(): void;
|
|
9
|
+
}
|
package/dist/failover.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AbstractProvider } from "ethers";
|
|
2
|
+
/**
|
|
3
|
+
* Failover ESTRITO por-erro, ordenado por CUSTO — substitui o `FallbackProvider` do ethers.
|
|
4
|
+
*
|
|
5
|
+
* O `FallbackProvider` é latência-based: ele raceia os providers e penaliza (backoff) o mais lento,
|
|
6
|
+
* então acaba preferindo o mais RÁPIDO (tipicamente o Alchemy direto) em vez do mais BARATO (Rabby/público).
|
|
7
|
+
* Isso queima CU pago à toa. Aqui a regra é estrita por prioridade:
|
|
8
|
+
*
|
|
9
|
+
* tenta subs[0]; só cai pro próximo se ele LANÇAR um erro de TRANSPORTE (403/429/timeout/socket).
|
|
10
|
+
*
|
|
11
|
+
* REGRAS (review adversarial do money-path):
|
|
12
|
+
* - Erro SEMÂNTICO do nó (revert/CALL_EXCEPTION, nonce too low, fundos insuf., já conhecido…) é uma resposta
|
|
13
|
+
* DEFINITIVA — re-lança IMEDIATAMENTE, sem failover. Tentar outro provider só mascararia o erro real
|
|
14
|
+
* (todos responderiam igual) e desperdiçaria chamadas.
|
|
15
|
+
* - `broadcastTransaction` é SINGLE-SHOT no provider primário (subs[0]): re-enviar a MESMA tx assinada noutro
|
|
16
|
+
* provider é idempotente por hash, mas um "already known" lançado como erro faria um broadcast bem-sucedido
|
|
17
|
+
* PARECER falha → o chamador re-tentaria (no deploy: colisão CREATE2; no swap: nonce já gasto). Em "speed",
|
|
18
|
+
* subs[0] é o pago/confiável (Alchemy); deploy/swap re-tentam em ciclos futuros se ele estiver fora.
|
|
19
|
+
*
|
|
20
|
+
* Resultado: Rabby (grátis) → públicos (grátis) → Alchemy (pago) só quando os grátis dão erro de TRANSPORTE.
|
|
21
|
+
* Todas as ops passam por `_perform` (getLogs, getBalance, call, estimateGas, getTransactionCount…) → cobertas.
|
|
22
|
+
*/
|
|
23
|
+
// Códigos de erro ethers v6 que são resposta DEFINITIVA do nó — NÃO faz failover (re-lança).
|
|
24
|
+
const NO_FAILOVER_CODES = new Set([
|
|
25
|
+
"CALL_EXCEPTION", // revert / execução falhou
|
|
26
|
+
"INSUFFICIENT_FUNDS",
|
|
27
|
+
"NONCE_EXPIRED", // "nonce too low" — a tx provavelmente já entrou
|
|
28
|
+
"REPLACEMENT_UNDERPRICED",
|
|
29
|
+
"TRANSACTION_REPLACED",
|
|
30
|
+
"UNCONFIGURED_NAME",
|
|
31
|
+
"ACTION_REJECTED",
|
|
32
|
+
]);
|
|
33
|
+
function shouldFailover(e) {
|
|
34
|
+
const code = e?.code;
|
|
35
|
+
// Sem code (ex.: Error cru do RabbyProvider em 429/timeout) = transporte → failover.
|
|
36
|
+
return !(code && NO_FAILOVER_CODES.has(code));
|
|
37
|
+
}
|
|
38
|
+
export class FailoverProvider extends AbstractProvider {
|
|
39
|
+
#subs;
|
|
40
|
+
#net;
|
|
41
|
+
constructor(subs, network) {
|
|
42
|
+
super(network);
|
|
43
|
+
this.#subs = subs;
|
|
44
|
+
this.#net = network;
|
|
45
|
+
}
|
|
46
|
+
// staticNetwork: nunca faz eth_chainId — todos os subs são da mesma rede, fixada na construção.
|
|
47
|
+
async _detectNetwork() {
|
|
48
|
+
return this.#net;
|
|
49
|
+
}
|
|
50
|
+
async _perform(req) {
|
|
51
|
+
// Escrita on-chain: single-shot no primário (ver doc acima) — nunca re-broadcast por failover.
|
|
52
|
+
if (req.method === "broadcastTransaction") {
|
|
53
|
+
return (await this.#subs[0]._perform(req));
|
|
54
|
+
}
|
|
55
|
+
let lastErr;
|
|
56
|
+
for (const s of this.#subs) {
|
|
57
|
+
try {
|
|
58
|
+
return (await s._perform(req));
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
lastErr = e;
|
|
62
|
+
if (!shouldFailover(e))
|
|
63
|
+
throw e; // erro semântico definitivo → re-lança já, sem tentar outro provider
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw lastErr;
|
|
67
|
+
}
|
|
68
|
+
destroy() {
|
|
69
|
+
for (const s of this.#subs) {
|
|
70
|
+
try {
|
|
71
|
+
s.destroy();
|
|
72
|
+
}
|
|
73
|
+
catch { /* ignore */ }
|
|
74
|
+
}
|
|
75
|
+
super.destroy();
|
|
76
|
+
}
|
|
77
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { WebSocketProvider } from "ethers";
|
|
2
|
+
import type { AbstractProvider } from "ethers";
|
|
3
|
+
import { HOLDGE_CHAINS, HOLDGE_CHAIN_KEYS, type HoldgeChainKey } from "./chains.js";
|
|
4
|
+
import { RabbyProvider } from "./rabby.js";
|
|
5
|
+
import { FailoverProvider } from "./failover.js";
|
|
6
|
+
export type HoldgeRpcProfile = "cost" | "speed";
|
|
7
|
+
export type HoldgeRpcOptions = {
|
|
8
|
+
/** Se setado, Alchemy entra na cadeia (pago). Em "cost" é o ÚLTIMO recurso; em "speed" é o PRIMEIRO. */
|
|
9
|
+
alchemyKey?: string;
|
|
10
|
+
/** Rabby na cadeia HTTP. Default true. */
|
|
11
|
+
useRabby?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* "cost" (default): grátis primeiro — Rabby → públicos → Alchemy(pago último). Pra reads de ALTO VOLUME
|
|
14
|
+
* (detecção/scan, polling de saldo) onde queimar CU pago seria caro.
|
|
15
|
+
* "speed": pago/confiável primeiro — Alchemy → Rabby (SEM públicos por default). Pra ops RARAS, urgentes e
|
|
16
|
+
* money-critical (deploy de contrato, swap de emergência) onde ms e correção importam mais que custo, e um
|
|
17
|
+
* RPC público flaky (nonce/recibo stale) seria perigoso. Deploys/swaps são poucos → custo desprezível.
|
|
18
|
+
*/
|
|
19
|
+
profile?: HoldgeRpcProfile;
|
|
20
|
+
/** Inclui os RPCs públicos na cadeia. Default: true em "cost", false em "speed". */
|
|
21
|
+
includePublic?: boolean;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Provider HTTP resiliente com `FailoverProvider` (estrito por-erro), NÃO o `FallbackProvider` do ethers
|
|
25
|
+
* (latência-based, racea e prefere o mais rápido = Alchemy pago). Só cai pro próximo quando o atual LANÇA.
|
|
26
|
+
*
|
|
27
|
+
* Dois perfis (ver `HoldgeRpcOptions.profile`):
|
|
28
|
+
* - "cost" → Rabby (grátis) → públicos (grátis) → Alchemy (pago, último). Reads de alto volume.
|
|
29
|
+
* - "speed" → Alchemy (pago/confiável) → Rabby → (públicos só se includePublic). Deploy/swap urgente.
|
|
30
|
+
*/
|
|
31
|
+
export declare function httpProvider(key: HoldgeChainKey, opts?: HoldgeRpcOptions): AbstractProvider;
|
|
32
|
+
/**
|
|
33
|
+
* Provider WSS (newHeads / subscriptions): Alchemy (se `alchemyKey`) senão público.
|
|
34
|
+
* Rabby NÃO tem WebSocket — pra detecção on-chain ao vivo use isto + `httpProvider` pra getLogs.
|
|
35
|
+
*/
|
|
36
|
+
export declare function wssProvider(key: HoldgeChainKey, opts?: HoldgeRpcOptions): WebSocketProvider;
|
|
37
|
+
export { HOLDGE_CHAINS, HOLDGE_CHAIN_KEYS, RabbyProvider, FailoverProvider };
|
|
38
|
+
export type { HoldgeChainKey, HoldgeChain } from "./chains.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { JsonRpcProvider, WebSocketProvider, Network } from "ethers";
|
|
2
|
+
import { HOLDGE_CHAINS, HOLDGE_CHAIN_KEYS } from "./chains.js";
|
|
3
|
+
import { RabbyProvider } from "./rabby.js";
|
|
4
|
+
import { FailoverProvider } from "./failover.js";
|
|
5
|
+
/**
|
|
6
|
+
* Provider HTTP resiliente com `FailoverProvider` (estrito por-erro), NÃO o `FallbackProvider` do ethers
|
|
7
|
+
* (latência-based, racea e prefere o mais rápido = Alchemy pago). Só cai pro próximo quando o atual LANÇA.
|
|
8
|
+
*
|
|
9
|
+
* Dois perfis (ver `HoldgeRpcOptions.profile`):
|
|
10
|
+
* - "cost" → Rabby (grátis) → públicos (grátis) → Alchemy (pago, último). Reads de alto volume.
|
|
11
|
+
* - "speed" → Alchemy (pago/confiável) → Rabby → (públicos só se includePublic). Deploy/swap urgente.
|
|
12
|
+
*/
|
|
13
|
+
export function httpProvider(key, opts = {}) {
|
|
14
|
+
const c = HOLDGE_CHAINS[key];
|
|
15
|
+
const useRabby = opts.useRabby !== false;
|
|
16
|
+
const profile = opts.profile ?? "cost";
|
|
17
|
+
// públicos: default true em "cost"; em "speed" default false (pago/Rabby bastam) — MAS se NÃO houver Alchemy,
|
|
18
|
+
// inclui os públicos mesmo em "speed", pra não degradar pra single-provider sem failover (resiliência > pureza).
|
|
19
|
+
const includePublic = opts.includePublic ?? (profile === "cost" || !opts.alchemyKey);
|
|
20
|
+
const rabby = useRabby ? new RabbyProvider(c.rabbyId, c.chainId) : null;
|
|
21
|
+
const alchemy = opts.alchemyKey
|
|
22
|
+
? new JsonRpcProvider(`https://${c.alchemyNet}.g.alchemy.com/v2/${opts.alchemyKey}`, c.chainId, { batchMaxCount: 1, staticNetwork: true })
|
|
23
|
+
: null;
|
|
24
|
+
const publics = includePublic
|
|
25
|
+
? c.publicHttp.map((url) => new JsonRpcProvider(url, c.chainId, { batchMaxCount: 1, staticNetwork: true }))
|
|
26
|
+
: [];
|
|
27
|
+
// "speed": pago/confiável primeiro. "cost": grátis primeiro, pago por último.
|
|
28
|
+
const ordered = profile === "speed" ? [alchemy, rabby, ...publics] : [rabby, ...publics, alchemy];
|
|
29
|
+
const subs = ordered.filter((p) => p !== null);
|
|
30
|
+
if (subs.length === 0)
|
|
31
|
+
throw new Error(`holdge-rpc: sem provider HTTP p/ ${key} (profile=${profile})`);
|
|
32
|
+
if (subs.length === 1)
|
|
33
|
+
return subs[0]; // 1 só → sem overhead do failover
|
|
34
|
+
return new FailoverProvider(subs, Network.from(c.chainId));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Provider WSS (newHeads / subscriptions): Alchemy (se `alchemyKey`) senão público.
|
|
38
|
+
* Rabby NÃO tem WebSocket — pra detecção on-chain ao vivo use isto + `httpProvider` pra getLogs.
|
|
39
|
+
*/
|
|
40
|
+
export function wssProvider(key, opts = {}) {
|
|
41
|
+
const c = HOLDGE_CHAINS[key];
|
|
42
|
+
const url = opts.alchemyKey ? `wss://${c.alchemyNet}.g.alchemy.com/v2/${opts.alchemyKey}` : c.publicWss;
|
|
43
|
+
return new WebSocketProvider(url, c.chainId);
|
|
44
|
+
}
|
|
45
|
+
export { HOLDGE_CHAINS, HOLDGE_CHAIN_KEYS, RabbyProvider, FailoverProvider };
|
package/dist/rabby.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { JsonRpcProvider } from "ethers";
|
|
2
|
+
import type { JsonRpcPayload, JsonRpcResult } from "ethers";
|
|
3
|
+
export declare class RabbyProvider extends JsonRpcProvider {
|
|
4
|
+
readonly rabbyId: string;
|
|
5
|
+
constructor(rabbyId: string, chainId: number);
|
|
6
|
+
_send(payload: JsonRpcPayload | JsonRpcPayload[]): Promise<JsonRpcResult[]>;
|
|
7
|
+
}
|
package/dist/rabby.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { JsonRpcProvider, Network } from "ethers";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
/**
|
|
4
|
+
* Provider ethers v6 que fala com a API Rabby (proxy de RPC, cobre ~103 EVMs incl. ETH/Polygon/Base/Arbitrum).
|
|
5
|
+
*
|
|
6
|
+
* A Rabby NÃO é um endpoint JSON-RPC padrão: o `chain_id` vai no CORPO (não na URL). Por isso sobrescrevemos
|
|
7
|
+
* `_send` pra montar `{ chain_id, method, params, jsonrpc, id }` em cada chamada.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANTE — POR QUE `node:https` E NÃO `fetch`:
|
|
10
|
+
* O WAF da Rabby (server `dbkserver` atrás de CloudFront) BLOQUEIA (403) o `fetch` do Node (undici) por
|
|
11
|
+
* fingerprint TLS/HTTP — independe de User-Agent/headers. O módulo `https` nativo do Node passa (200).
|
|
12
|
+
* Verificado empiricamente: `curl` e `https` nativo → 200+JSON; `fetch`/undici → 403 página de bloqueio.
|
|
13
|
+
* (O GroupPay usa `fetch` e por isso está com Rabby OFFLINE — este provider o supera.)
|
|
14
|
+
* CONSEQUÊNCIA: este provider é **server-side (Node) apenas** — não roda em browser/edge runtime.
|
|
15
|
+
*
|
|
16
|
+
* `batchMaxCount: 1` desliga o batch (a Rabby aceita 1 chamada por POST). `staticNetwork` evita um
|
|
17
|
+
* eth_chainId na construção. NÃO suporta WebSocket (use o wssProvider pra newHeads/subscriptions).
|
|
18
|
+
*/
|
|
19
|
+
const RABBY_URL = "https://api.rabby.io/v1/wallet/eth_rpc";
|
|
20
|
+
const RABBY_HOST = "api.rabby.io";
|
|
21
|
+
const RABBY_PATH = "/v1/wallet/eth_rpc";
|
|
22
|
+
const TIMEOUT_MS = 15_000;
|
|
23
|
+
// keep-alive: o watcher faz muitas chamadas; reusar a conexão TLS evita handshake por request.
|
|
24
|
+
const agent = new https.Agent({ keepAlive: true, maxSockets: 8 });
|
|
25
|
+
function rabbyPost(bodyObj) {
|
|
26
|
+
const body = JSON.stringify(bodyObj);
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const req = https.request({ host: RABBY_HOST, path: RABBY_PATH, method: "POST", agent, timeout: TIMEOUT_MS,
|
|
29
|
+
headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) } }, (res) => {
|
|
30
|
+
let buf = "";
|
|
31
|
+
res.on("data", (c) => (buf += c));
|
|
32
|
+
res.on("end", () => {
|
|
33
|
+
if (res.statusCode !== 200)
|
|
34
|
+
return resolve({ status: res.statusCode ?? 0, json: null });
|
|
35
|
+
try {
|
|
36
|
+
resolve({ status: 200, json: JSON.parse(buf) });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
reject(new Error(`Rabby resposta inválida (${RABBY_HOST})`));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
req.on("error", reject);
|
|
44
|
+
req.on("timeout", () => req.destroy(new Error("Rabby timeout")));
|
|
45
|
+
req.write(body);
|
|
46
|
+
req.end();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export class RabbyProvider extends JsonRpcProvider {
|
|
50
|
+
rabbyId;
|
|
51
|
+
constructor(rabbyId, chainId) {
|
|
52
|
+
super(RABBY_URL, Network.from(chainId), { batchMaxCount: 1, staticNetwork: true });
|
|
53
|
+
this.rabbyId = rabbyId;
|
|
54
|
+
}
|
|
55
|
+
// ethers tipa o retorno como JsonRpcResult[], mas em runtime aceita {id,error} (ele checa .error e lança).
|
|
56
|
+
async _send(payload) {
|
|
57
|
+
const reqs = Array.isArray(payload) ? payload : [payload];
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const p of reqs) {
|
|
60
|
+
const { status, json } = await rabbyPost({ chain_id: this.rabbyId, method: p.method, params: p.params ?? [], jsonrpc: "2.0", id: p.id });
|
|
61
|
+
if (status !== 200 || !json)
|
|
62
|
+
throw new Error(`Rabby proxy HTTP ${status} (${this.rabbyId})`);
|
|
63
|
+
if (json.error)
|
|
64
|
+
out.push({ id: p.id, error: json.error });
|
|
65
|
+
else
|
|
66
|
+
out.push({ id: p.id, result: json.result });
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "holdge-rpc",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "RPC provider resiliente para apps Holdge (server-side): Rabby via node:https → públicos → Alchemy. Failover estrito por-erro + perfis cost/speed. HTTP + WSS.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"ethers": "^6"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.10.1",
|
|
29
|
+
"ethers": "^6.13.4",
|
|
30
|
+
"typescript": "^5.7.2"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/samyrwendel/holdge-rpc.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/samyrwendel/holdge-rpc/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/samyrwendel/holdge-rpc#readme"
|
|
41
|
+
}
|