*Initial version

This commit is contained in:
Derrick Hammer 2022-06-27 15:36:29 -04:00
parent 8841f5aa66
commit 289cd2f5cc
8 changed files with 270 additions and 2 deletions

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2022 Lume Web Copyright (c) 2022 Hammer Technologies LLC
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,2 +1,10 @@
# dht-rpc-client # dht-rpc-client
A client library that uses hypercore and the @lumeweb/relay server along with Skynet for web, to perform "Wisdom of the crowd" RPC requests. A client library that uses hypercore and the https://github.com/LumeWeb/relay server along with Skynet for web, to perform `Wisdom of the crowd` RPC requests.
This enables access to blockchain RPC without running a node, and socializes the cost of access to RPC from use of services such as https://pokt.dht
As demand grows for users, so should the community. It is expected that both businesses operating on web3 and community members donating/supporting in altruism will ensure the upkeep of this dht.
It is the projects hope that blockchains will evolve in the future such that much of this infrastructure becomes unneeded and RPC can be done directly with light clients. This would also need to support over Websockets like how Webtorrent works.
As very few blockchains actually support this and for use with decentralized nodes, this type of dht/technology is required for mainstream adoption.

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "@lumeweb/dht-rpc-client",
"type": "module",
"version": "0.1.0",
"description": "",
"main": "dist/index.js",
"devDependencies": {
"@types/node": "^18.0.0"
},
"dependencies": {
"@hyperswarm/dht": "^6.0.1",
"msgpackr": "^1.6.1"
}
}

3
src/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from "./rpcNetwork.js";
export * from "./rpcQuery.js";
export * from "./types";

85
src/rpcNetwork.ts Normal file
View File

@ -0,0 +1,85 @@
// tslint:disable:no-var-requires
import { createRequire } from "module";
import RpcQuery from "./rpcQuery.js";
const require = createRequire(import.meta.url);
const DHT = require("@hyperswarm/dht");
export default class RpcNetwork {
private _dht: typeof DHT;
private _majorityThreshold = 0.75;
private _maxTtl = 12 * 60 * 60;
private _queryTimeout = 30;
private _relays: string[] = [];
private _ready: Promise<void>;
private _force: boolean = false;
constructor(dht = new DHT()) {
this._dht = dht;
this._ready = this._dht.ready()
}
get ready(): Promise<void> {
return this._ready;
}
get relays(): string[] {
return this._relays;
}
get dht() {
return this._dht;
}
get maxTtl(): number {
return this._maxTtl;
}
set maxTtl(value: number) {
this._maxTtl = value;
}
get queryTimeout(): number {
return this._queryTimeout;
}
set queryTimeout(value: number) {
this._queryTimeout = value;
}
get majorityThreshold(): number {
return this._majorityThreshold;
}
set majorityThreshold(value: number) {
this._majorityThreshold = value;
}
get force(): boolean {
return this._force;
}
set force(value: boolean) {
this._force = value;
}
public addRelay(pubkey: string): void {
this._relays.push(pubkey);
this._relays = [...new Set(this._relays)];
}
public query(
query: string,
chain: string,
data: object | any[] = {},
force: boolean = false
): RpcQuery {
return new RpcQuery(this, {
query,
chain,
data,
force: force || this._force,
});
}
}

120
src/rpcQuery.ts Normal file
View File

@ -0,0 +1,120 @@
import { clearTimeout, setTimeout } from "timers";
import RpcNetwork from "./rpcNetwork.js";
import { pack, unpack } from "msgpackr";
import {RPCRequest, RPCResponse} from "./types";
export default class RpcQuery {
private _network: RpcNetwork;
private _query: RPCRequest;
private _promise?: Promise<any>;
private _timeoutTimer?: any;
private _timeout: boolean = false;
private _completed: boolean = false;
private _responses: { [relay: string]: RPCResponse } = {};
private _promiseResolve?: (data: any) => void;
constructor(network: RpcNetwork, query: RPCRequest) {
this._network = network;
this._query = query;
this.init();
}
get promise(): Promise<any> {
return this._promise as Promise<any>;
}
private handeTimeout() {
this.resolve(false, true);
}
private resolve(data: any, timeout: boolean = false): void {
clearTimeout(this._timeoutTimer);
this._timeout = timeout;
this._completed = true;
// @ts-ignore
this._promiseResolve(data);
}
private async init() {
this._promise =
this._promise ??
new Promise<any>((resolve) => {
this._promiseResolve = resolve;
});
this._timeoutTimer =
this._timeoutTimer ??
setTimeout(
this.handeTimeout.bind(this),
this._network.queryTimeout * 1000
);
await this._network.ready;
const promises = [];
// tslint:disable-next-line:forin
for (const relay of this._network.relays) {
promises.push(this.queryRelay(relay));
}
await Promise.allSettled(promises);
this.checkResponses();
}
private async queryRelay(relay: string): Promise<any> {
const socket = this._network.dht.connect(Buffer.from(relay, "hex"));
return new Promise((resolve, reject) => {
socket.on("data", (res: Buffer) => {
socket.end();
const response = unpack(res);
if (response && response.error) {
return reject(response);
}
this._responses[relay] = response;
resolve(null);
});
socket.on("error", (error: any) => reject({ error }));
socket.write(pack(this._query));
});
}
private checkResponses() {
const responses: { [response: string]: number } = {};
const responseStore = this._responses;
const responseStoreKeys = Object.keys(responseStore);
// tslint:disable-next-line:forin
for (const peer in responseStore) {
const responseIndex = responseStoreKeys.indexOf(peer);
responses[responseIndex] = responses[responseIndex] ?? 0;
responses[responseIndex]++;
}
for (const responseIndex in responses) {
if (responses[responseIndex] / responseStoreKeys.length >= this._network.majorityThreshold) {
const response: RPCResponse | null =
responseStore[responseStoreKeys[parseInt(responseIndex, 10)]];
// @ts-ignore
if (null === response || null === response?.data) {
this.retry();
return;
}
this.resolve(response?.data);
}
}
}
private retry() {
this._responses = {};
if (this._completed) {
return;
}
this.init();
}
}

15
src/types.ts Normal file
View File

@ -0,0 +1,15 @@
export interface RPCRequest {
force: boolean;
chain: string;
query: string;
data: any;
}
export interface RPCResponse {
updated: number;
data:
| any
| {
error: string | boolean;
};
}

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"declaration": true,
"strict": true,
"module": "esnext",
"target": "esnext",
"esModuleInterop": true,
"sourceMap": false,
"rootDir": "src",
"outDir": "dist",
"typeRoots": [
"node_modules/@types",
],
"moduleResolution": "node",
"declarationMap": true,
"declarationDir": "dist",
"emitDeclarationOnly": false,
"allowJs": true
},
"include": [
"src"
]
}