*Initial version
This commit is contained in:
parent
8841f5aa66
commit
289cd2f5cc
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
10
README.md
10
README.md
|
@ -1,2 +1,10 @@
|
|||
# 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.
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./rpcNetwork.js";
|
||||
export * from "./rpcQuery.js";
|
||||
export * from "./types";
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue