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 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
+ ```
@@ -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
+ }
@@ -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
+ }
@@ -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 };
@@ -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
+ }