*Initial merge of rpcproxy

This commit is contained in:
Derrick Hammer 2022-07-04 19:17:58 -04:00
parent 660bb85203
commit 8d95dde848
12 changed files with 592 additions and 25 deletions

View File

@ -11,27 +11,35 @@
"dependencies": { "dependencies": {
"@hyperswarm/dht": "^6.0.1", "@hyperswarm/dht": "^6.0.1",
"@hyperswarm/dht-relay": "^0.3.0", "@hyperswarm/dht-relay": "^0.3.0",
"@pokt-network/pocket-js": "^0.8.0-rc",
"@root/greenlock": "^4.0.5", "@root/greenlock": "^4.0.5",
"@solana/web3.js": "^1.47.3",
"@types/node": "^18.0.0", "@types/node": "^18.0.0",
"@types/node-cron": "^3.0.2", "@types/node-cron": "^3.0.2",
"@types/ws": "^8.5.3", "@types/ws": "^8.5.3",
"@types/xml2js": "^0.4.11", "@types/xml2js": "^0.4.11",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"dotenv": "^16.0.1",
"ethers": "^5.6.9",
"express": "^4.18.1", "express": "^4.18.1",
"greenlock-express": "^4.0.3", "greenlock-express": "^4.0.3",
"jayson": "^3.6.6", "jayson": "^3.6.6",
"json-stable-stringify": "^1.0.1", "json-stable-stringify": "^1.0.1",
"libskynet": "^0.0.48", "libskynet": "^0.0.48",
"libskynetnode": "^0.1.3", "libskynetnode": "^0.1.3",
"minimatch": "^5.1.0",
"msgpackr": "^1.6.1", "msgpackr": "^1.6.1",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"node-cron": "^3.0.1", "node-cron": "^3.0.1",
"node-fetch": "^3.2.6", "node-fetch": "^3.2.6",
"random-access-memory": "^4.1.0", "random-access-memory": "^4.1.0",
"sprintf-js": "^1.1.2",
"xml2js": "^0.4.23" "xml2js": "^0.4.23"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/minimatch": "^3.0.5",
"@types/sprintf-js": "^1.1.2",
"hyper-typings": "^1.0.0", "hyper-typings": "^1.0.0",
"prettier": "^2.7.1" "prettier": "^2.7.1"
} }

View File

@ -1,5 +1,33 @@
import fs from "fs";
import dotenv from "dotenv";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const envPath = path.resolve(__dirname, "..", "lumerelay.env");
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
}
export const RELAY_PORT = process.env.RELAY_PORT ?? 8080; export const RELAY_PORT = process.env.RELAY_PORT ?? 8080;
export const RELAY_DOMAIN = process.env.RELAY_DOMAIN; export const RELAY_DOMAIN = process.env.RELAY_DOMAIN;
export const AFRAID_USERNAME = process.env.AFRAID_USERNAME; export const AFRAID_USERNAME = process.env.AFRAID_USERNAME;
export const AFRAID_PASSWORD = process.env.AFRAID_PASSWORD; export const AFRAID_PASSWORD = process.env.AFRAID_PASSWORD;
export const RELAY_SEED = process.env.RELAY_SEED; export const RELAY_SEED = process.env.RELAY_SEED;
export const POCKET_APP_ID = process.env.POCKET_APP_ID || false;
export const POCKET_APP_KEY = process.env.POCKET_APP_KEY || false;
export const POCKET_ACCOUNT_PUBLIC_KEY =
process.env.POCKET_ACCOUNT_PUBLIC_KEY || false;
export const POCKET_ACCOUNT_PRIVATE_KEY =
process.env.POCKET_ACCOUNT_PRIVATE_KEY || false;
export const HSD_NETWORK_TYPE = process.env.HSD_NETWORK || "main";
export const HSD_HOST = process.env.HSD_HOST || "localhost";
export const HSD_PORT = Number(process.env.HSD_PORT) || 12037;
export const HSD_API_KEY = process.env.HSD_API_KEY || "foo";
export const POCKET_HOST = process.env.POCKET_HOST || "localhost";
export const POCKET_PORT = process.env.POCKET_PORT || 8081;

View File

@ -4,7 +4,7 @@ let error = false;
for (const constant in CONFIG) { for (const constant in CONFIG) {
// @ts-ignore // @ts-ignore
if (!CONFIG[constant]) { if (CONFIG[constant] === null || CONFIG[constant] === undefined) {
console.error(`Missing constant ${constant}`); console.error(`Missing constant ${constant}`);
error = true; error = true;
} }
@ -14,4 +14,14 @@ if (error) {
process.exit(1); process.exit(1);
} }
let usingPocketGateway = true;
export function usePocketGateway() {
return usingPocketGateway;
}
export function updateUsePocketGateway(state: boolean): void {
usingPocketGateway = state;
}
export * from "./constant_vars.js"; export * from "./constant_vars.js";

View File

@ -5,16 +5,41 @@ import { Mutex } from "async-mutex";
import { createRequire } from "module"; import { createRequire } from "module";
import NodeCache from "node-cache"; import NodeCache from "node-cache";
import { get as getDHT } from "./dht.js"; import { get as getDHT } from "./dht.js";
import {
POCKET_ACCOUNT_PRIVATE_KEY,
POCKET_ACCOUNT_PUBLIC_KEY,
POCKET_APP_ID,
POCKET_APP_KEY,
POCKET_HOST,
POCKET_PORT,
} from "./constant_vars";
import { updateUsePocketGateway, usePocketGateway } from "./constants";
import { Server as JSONServer } from "jayson/promise/index.js";
import { rpcMethods } from "./rpc/index.js";
import {
Configuration,
HttpRpcProvider,
Pocket,
PocketAAT,
} from "@pokt-network/pocket-js";
import {
JSONRPCRequest,
JSONRPCResponseWithError,
JSONRPCResponseWithResult,
} from "jayson";
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const stringify = require("json-stable-stringify"); const stringify = require("json-stable-stringify");
const clients: { [chain: string]: any } = {};
const pendingRequests = new NodeCache(); const pendingRequests = new NodeCache();
const processedRequests = new NodeCache({ const processedRequests = new NodeCache({
stdTTL: 60 * 60 * 12, stdTTL: 60 * 60 * 12,
}); });
let pocketServer: Pocket;
let _aat: PocketAAT;
let jsonServer: jayson.Server;
interface RPCRequest { interface RPCRequest {
force: boolean; force: boolean;
chain: string; chain: string;
@ -35,23 +60,6 @@ function hash(data: string): string {
return crypto.createHash("sha256").update(data).digest("hex"); return crypto.createHash("sha256").update(data).digest("hex");
} }
function getClient(chain: string): Function {
chain = chain.replace(/[^a-z0-9\-]/g, "");
if (!(chain in clients)) {
clients[chain] = jayson.Client.http({
host: process.env.RPC_PROXY_HOST,
port: parseInt(process.env.RPC_PROXY_PORT as string),
path: "/",
headers: {
"X-Chain": chain,
},
});
}
return clients[chain];
}
function getRequestId(request: RPCRequest) { function getRequestId(request: RPCRequest) {
const clonedRequest = Object.assign({}, request); const clonedRequest = Object.assign({}, request);
@ -98,10 +106,13 @@ async function processRequest(request: RPCRequest): Promise<RPCResponse> {
let error; let error;
try { try {
// @ts-ignore rpcResp = await processRpcRequest(
rpcResp = await getClient(request.chain).request( {
request.query, method: request.query,
request.data jsonrpc: "2.0",
params: request.data,
} as unknown as JSONRPCRequest,
request.chain
); );
} catch (e) { } catch (e) {
error = (e as Error).message; error = (e as Error).message;
@ -113,15 +124,22 @@ async function processRequest(request: RPCRequest): Promise<RPCResponse> {
}; };
if (rpcResp) { if (rpcResp) {
rpcResp = rpcResp as JSONRPCResponseWithResult;
if (false === rpcResp.result) { if (false === rpcResp.result) {
error = true; error = true;
} }
rpcResp = rpcResp as unknown as JSONRPCResponseWithError;
if (rpcResp.error) { if (rpcResp.error) {
// @ts-ignore
error = rpcResp.error.message; error = rpcResp.error.message;
} }
} }
dbData.data = error ? { error } : rpcResp.result; dbData.data = error
? { error }
: (rpcResp as unknown as JSONRPCResponseWithResult).result;
if (!processedRequests.get(reqId) || request.force) { if (!processedRequests.get(reqId) || request.force) {
processedRequests.set(reqId, dbData); processedRequests.set(reqId, dbData);
@ -132,7 +150,93 @@ async function processRequest(request: RPCRequest): Promise<RPCResponse> {
return dbData; return dbData;
} }
export function updateAat(aat: PocketAAT): void {
_aat = aat;
}
export function getAat(): PocketAAT {
return _aat;
}
export function getPocketServer(): Pocket {
return pocketServer;
}
export async function unlockAccount(
accountPrivateKey: string,
accountPublicKey: string,
accountPassphrase: string
): Promise<PocketAAT> {
try {
const account = await pocketServer.keybase.importAccount(
Buffer.from(accountPrivateKey, "hex"),
accountPassphrase
);
if (account instanceof Error) {
// noinspection ExceptionCaughtLocallyJS
throw account;
}
await pocketServer.keybase.unlockAccount(
account.addressHex,
accountPassphrase,
0
);
return await PocketAAT.from(
"0.0.1",
accountPublicKey,
accountPublicKey,
accountPrivateKey
);
} catch (e) {
console.error(e);
process.exit(1);
}
}
export async function processRpcRequest(
request: JSONRPCRequest,
chain: string
): Promise<JSONRPCResponseWithResult | JSONRPCResponseWithError | undefined> {
return new Promise((resolve) => {
jsonServer.call(
request,
{ chain },
(
err?: JSONRPCResponseWithError | null,
result?: JSONRPCResponseWithResult
): void => {
if (err) {
return resolve(err);
}
resolve(result);
}
);
});
}
export async function start() { export async function start() {
if (!POCKET_APP_ID || !POCKET_APP_KEY) {
const dispatchURL = new URL(`http://${POCKET_HOST}:${POCKET_PORT}`);
const rpcProvider = new HttpRpcProvider(dispatchURL);
const configuration = new Configuration();
pocketServer = new Pocket([dispatchURL], rpcProvider, configuration);
updateUsePocketGateway(false);
}
if (!usePocketGateway()) {
updateAat(
await unlockAccount(
<string>POCKET_ACCOUNT_PRIVATE_KEY,
<string>POCKET_ACCOUNT_PUBLIC_KEY,
"0"
)
);
}
jsonServer = new JSONServer(rpcMethods, { useContext: true });
(await getDHT()).on("connection", (socket: any) => { (await getDHT()).on("connection", (socket: any) => {
socket.on("data", async (data: any) => { socket.on("data", async (data: any) => {
let request: RPCRequest; let request: RPCRequest;

112
src/rpc/algorand.ts Normal file
View File

@ -0,0 +1,112 @@
import { maybeMapChainId, reverseMapChainId } from "../util.js";
import minimatch from "minimatch";
// @ts-ignore
import HTTPClient from "algosdk/dist/cjs/src/client/client.js";
import { sprintf } from "sprintf-js";
import { RpcMethodList } from "./index.js";
import { POCKET_APP_ID } from "../constants.js";
const allowedEndpoints: { [endpoint: string]: ("GET" | "POST")[] } = {
"/v2/teal/compile": ["POST"],
"/v2/accounts/*": ["GET"],
};
export function proxyRestMethod(
apiServer: string,
matchChainId: string
): Function {
return async function (args: any, context: object) {
// @ts-ignore
let chain = context.chain;
let chainId = maybeMapChainId(chain);
if (!chainId) {
throw new Error("Invalid Chain");
}
chainId = reverseMapChainId(chainId as string);
if (!chainId || chainId !== matchChainId) {
throw new Error("Invalid Chain");
}
let method = args.method ?? false;
let endpoint = args.endpoint ?? false;
let data = args.data ?? false;
let query = args.query ?? false;
let fullHeaders = args.fullHeaders ?? {};
fullHeaders = { ...fullHeaders, Referer: "lumeweb_dns_relay" };
if (method) {
method = method.toUpperCase();
}
if (!endpoint) {
throw new Error("Endpoint Missing");
}
let found = false;
for (const theEndpoint in allowedEndpoints) {
if (minimatch(endpoint, theEndpoint)) {
found = true;
break;
}
}
if (!found) {
throw new Error("Endpoint Invalid");
}
let apiUrl;
try {
apiUrl = sprintf(apiServer, chainId, POCKET_APP_ID);
} catch (e) {
apiUrl = apiServer;
}
const client = new HTTPClient({}, apiUrl);
let resp;
switch (method) {
case "GET":
resp = await client.get(endpoint, query, fullHeaders);
break;
case "POST":
if (Array.isArray(data?.data)) {
data = new Uint8Array(Buffer.from(data.data));
}
resp = await client.post(endpoint, data, { ...fullHeaders });
break;
default:
throw new Error("Method Invalid");
}
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key: string, value: any): any => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
return JSON.parse(JSON.stringify(resp, getCircularReplacer()));
};
}
export default {
algorand_rest_request: proxyRestMethod(
"http://mainnet-api.algonode.network",
"algorand-mainnet"
),
//'algorand_rest_request': proxyRestMethod("https://%s.gateway.pokt.network/v1/lb/%s", "algorand-mainnet"),
algorand_rest_indexer_request: proxyRestMethod(
"http://mainnet-idx.algonode.network",
"algorand-mainnet-indexer"
),
} as RpcMethodList;

100
src/rpc/common.ts Normal file
View File

@ -0,0 +1,100 @@
import { ethers } from "ethers";
import { PocketAAT } from "@pokt-network/pocket-js";
import { maybeMapChainId, reverseMapChainId } from "../util.js";
import { Connection } from "@solana/web3.js";
import {
POCKET_APP_ID,
POCKET_APP_KEY,
usePocketGateway,
} from "../constants.js";
import { getAat, getPocketServer } from "../rpc.js";
export const chainNetworks = require("../../networks.json");
type RpcProviderMethod = (method: string, params: Array<any>) => Promise<any>;
const gatewayProviders: { [name: string]: RpcProviderMethod } = {};
const gatewayMethods: {
[name: string]: (chainId: string) => RpcProviderMethod;
} = {
default: (chainId: string): RpcProviderMethod => {
const provider = new ethers.providers.JsonRpcProvider({
url: `https://${chainId}.gateway.pokt.network/v1/lb/${POCKET_APP_ID}`,
password: <string>POCKET_APP_KEY,
});
return provider.send.bind(provider);
},
"sol-mainnet": (chainId: string): RpcProviderMethod => {
const provider = new Connection(
`https://solana-mainnet.gateway.pokt.network/v1/lb/${POCKET_APP_ID}`
);
// @ts-ignore
return provider._rpcRequest.bind(provider);
},
};
export function proxyRpcMethod(
method: string,
chains: string[] = []
): Function {
return async function (args: any, context: object) {
// @ts-ignore
let chain = context.chain;
let chainId = maybeMapChainId(chain);
let chainMatch = true;
if (
chains.length > 0 &&
!chains.includes(chain) &&
!chains.includes(chainId.toString())
) {
chainMatch = false;
}
if (!chainId || !chainMatch) {
throw new Error("Invalid Chain");
}
if (usePocketGateway()) {
chainId = reverseMapChainId(chainId as string);
if (!chainId) {
throw new Error("Invalid Chain");
}
let provider: RpcProviderMethod | boolean =
gatewayProviders[chainId as string] || false;
if (!provider) {
provider = getRpcProvider(chainId as string);
}
gatewayProviders[chainId as string] = provider;
return await provider(method, args);
}
return await sendRelay(JSON.stringify(args), <string>chainId, getAat());
};
}
// Call this every time you want to fetch RPC data
async function sendRelay(
rpcQuery: string,
blockchain: string,
pocketAAT: PocketAAT
) {
try {
return await getPocketServer().sendRelay(rpcQuery, blockchain, pocketAAT);
} catch (e) {
console.log(e);
throw e;
}
}
function getRpcProvider(chain: string): RpcProviderMethod {
if (chain in gatewayMethods) {
return gatewayMethods[chain](chain);
}
return gatewayMethods.default(chain);
}

105
src/rpc/dns.ts Normal file
View File

@ -0,0 +1,105 @@
import { isIp } from "../util.js";
import { RpcMethodList } from "./index.js";
const bns = require("bns");
const { StubResolver, RecursiveResolver } = bns;
const resolverOpt = {
tcp: true,
inet6: false,
edns: true,
dnssec: true,
};
const globalResolver = new RecursiveResolver(resolverOpt);
globalResolver.hints.setDefault();
globalResolver.open();
async function resolveNameServer(ns: string): Promise<string | boolean> {
if (isIp(ns)) {
return ns;
}
let result = await getDnsRecords(ns, "A");
if (result.length) {
return result[0];
}
return false;
}
async function getDnsRecords(
domain: string,
type: string,
authority: boolean = false,
resolver = globalResolver
): Promise<string[]> {
let result;
try {
result = await resolver.lookup(domain, type);
} catch (e) {
return [];
}
let prop = authority ? "authority" : "answer";
if (!result || !result[prop].length) {
return [];
}
return result[prop].map(
(item: object) =>
// @ts-ignore
item.data.address ?? item.data.target ?? item.data.ns ?? null
);
}
export default {
dnslookup: async function (args: any) {
let dnsResults: string[] = [];
let domain = args.domain;
let ns = args.nameserver;
let dnsResolver = ns ? new StubResolver(resolverOpt) : globalResolver;
await dnsResolver.open();
if (ns) {
let nextNs = ns;
let prevNs = null;
while (nextNs) {
nextNs = await resolveNameServer(nextNs);
if (!nextNs) {
nextNs = prevNs;
}
dnsResolver.setServers([nextNs]);
if (nextNs === prevNs) {
break;
}
let result = await getDnsRecords(domain, "NS", true, dnsResolver);
prevNs = nextNs;
nextNs = result.length ? result[0] : false;
}
}
for (const queryType of ["CNAME", "A"]) {
let result = await getDnsRecords(domain, queryType, false, dnsResolver);
if (result) {
dnsResults = dnsResults.concat(result);
}
}
await dnsResolver.close();
dnsResults = dnsResults.filter(Boolean);
if (dnsResults.length) {
return dnsResults[0];
}
return false;
},
} as RpcMethodList;

14
src/rpc/evm.ts Normal file
View File

@ -0,0 +1,14 @@
import { proxyRpcMethod } from "./common.js";
import { RpcMethodList } from "./index.js";
const rpcMethods: RpcMethodList = {};
function proxyEvmRpcMethod(method: string): Function {
return proxyRpcMethod(method);
}
["eth_call", "eth_chainId", "net_version"].forEach((method) => {
rpcMethods[method] = proxyEvmRpcMethod(method);
});
export default rpcMethods;

27
src/rpc/handshake.ts Normal file
View File

@ -0,0 +1,27 @@
import { RpcMethodList } from "./index.js";
import {
HSD_API_KEY,
HSD_HOST,
HSD_NETWORK_TYPE,
HSD_PORT,
} from "../constant_vars.js";
const { NodeClient } = require("hs-client");
const hnsClient = new NodeClient({
network: HSD_NETWORK_TYPE,
host: HSD_HOST,
port: HSD_PORT,
apiKey: HSD_API_KEY,
});
export default {
getnameresource: async function (args: any, context: object) {
// @ts-ignore
if ("hns" !== context.chain) {
throw new Error("Invalid Chain");
}
return await hnsClient.execute("getnameresource", args);
},
} as RpcMethodList;

18
src/rpc/index.ts Normal file
View File

@ -0,0 +1,18 @@
export type RpcMethodList = { [name: string]: Function };
export * from "./common.js";
import { default as DnsMethods } from "./dns.js";
import { default as EvmMethods } from "./evm.js";
import { default as HnsMethods } from "./handshake.js";
import { default as SolMethods } from "./solana.js";
import { default as AlgoMethods } from "./algorand.js";
export const rpcMethods: RpcMethodList = Object.assign(
{},
DnsMethods,
EvmMethods,
HnsMethods,
SolMethods,
AlgoMethods
);

8
src/rpc/solana.ts Normal file
View File

@ -0,0 +1,8 @@
import { chainNetworks, proxyRpcMethod } from "./common.js";
import { RpcMethodList } from "./index.js";
export default {
getAccountInfo: proxyRpcMethod("getAccountInfo", [
chainNetworks["sol-mainnet"],
]),
} as RpcMethodList;

View File

@ -1,4 +1,37 @@
import { chainNetworks } from "./rpc/index.js";
export function errorExit(msg: string): void { export function errorExit(msg: string): void {
console.error(msg); console.error(msg);
process.exit(1); process.exit(1);
} }
export function maybeMapChainId(chain: string): string | boolean {
if (chain in chainNetworks) {
return chainNetworks[chain];
}
if (
[parseInt(chain, 16).toString(), parseInt(chain, 10).toString()].includes(
chain.toLowerCase()
)
) {
return chain;
}
return false;
}
export function reverseMapChainId(chainId: string): string | boolean {
let vals = Object.values(chainNetworks);
if (!vals.includes(chainId)) {
return false;
}
return Object.keys(chainNetworks)[vals.indexOf(chainId)];
}
export function isIp(ip: string) {
return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(
ip
);
}