From 289cd2f5cc666ad7a59d97f2e496e0467436fa31 Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Mon, 27 Jun 2022 15:36:29 -0400 Subject: [PATCH] *Initial version --- LICENSE | 2 +- README.md | 10 +++- package.json | 14 ++++++ src/index.ts | 3 ++ src/rpcNetwork.ts | 85 ++++++++++++++++++++++++++++++++ src/rpcQuery.ts | 120 ++++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 15 ++++++ tsconfig.json | 23 +++++++++ 8 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 src/rpcNetwork.ts create mode 100644 src/rpcQuery.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json diff --git a/LICENSE b/LICENSE index 13a5b6e..8995c8b 100644 --- a/LICENSE +++ b/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 diff --git a/README.md b/README.md index 3c5d2bf..56486d6 100644 --- a/README.md +++ b/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. diff --git a/package.json b/package.json new file mode 100644 index 0000000..51ac1de --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..66e765c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from "./rpcNetwork.js"; +export * from "./rpcQuery.js"; +export * from "./types"; diff --git a/src/rpcNetwork.ts b/src/rpcNetwork.ts new file mode 100644 index 0000000..8f537e0 --- /dev/null +++ b/src/rpcNetwork.ts @@ -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; + private _force: boolean = false; + + constructor(dht = new DHT()) { + this._dht = dht; + this._ready = this._dht.ready() + } + + get ready(): Promise { + 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, + }); + } +} diff --git a/src/rpcQuery.ts b/src/rpcQuery.ts new file mode 100644 index 0000000..f189ea4 --- /dev/null +++ b/src/rpcQuery.ts @@ -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; + 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 { + return this._promise as Promise; + } + + 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((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 { + 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(); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..86c9f6d --- /dev/null +++ b/src/types.ts @@ -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; + }; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a4d5cd4 --- /dev/null +++ b/tsconfig.json @@ -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" + ] +}