151 lines
3.4 KiB
TypeScript
151 lines
3.4 KiB
TypeScript
|
import NodeCache from "node-cache";
|
||
|
import { PluginAPI } from "@lumeweb/interface-relay";
|
||
|
import stringify from "json-stringify-deterministic";
|
||
|
|
||
|
export type RPCRequest = {
|
||
|
method: string;
|
||
|
params: any[];
|
||
|
};
|
||
|
|
||
|
export type RPCRequestRaw = RPCRequest & {
|
||
|
jsonrpc: string;
|
||
|
id: string;
|
||
|
};
|
||
|
|
||
|
export type RPCResponse = {
|
||
|
success: boolean;
|
||
|
result: any;
|
||
|
};
|
||
|
|
||
|
export type ProviderConfig = {
|
||
|
URL: string;
|
||
|
unsupportedMethods?: string[];
|
||
|
supportBatchRequests?: boolean;
|
||
|
batchSize?: number;
|
||
|
};
|
||
|
|
||
|
export class RPC {
|
||
|
get cache(): NodeCache {
|
||
|
return this._cache;
|
||
|
}
|
||
|
private provider: ProviderConfig;
|
||
|
private _cache = new NodeCache({ stdTTL: 60 * 60 * 12 });
|
||
|
|
||
|
constructor(provider: ProviderConfig) {
|
||
|
this.provider = provider;
|
||
|
}
|
||
|
|
||
|
private _pluginApi?: PluginAPI;
|
||
|
|
||
|
set pluginApi(value: PluginAPI) {
|
||
|
this._pluginApi = value;
|
||
|
}
|
||
|
|
||
|
async request(request: RPCRequest) {
|
||
|
if (this.provider.unsupportedMethods?.includes(request.method)) {
|
||
|
throw new Error("method not supported by the provider");
|
||
|
}
|
||
|
return await this._retryRequest(request);
|
||
|
}
|
||
|
|
||
|
async requestBatch(requests: RPCRequest[]) {
|
||
|
if (
|
||
|
this.provider?.unsupportedMethods &&
|
||
|
requests.some((r) => this.provider.unsupportedMethods!.includes(r.method))
|
||
|
) {
|
||
|
throw new Error("method not supported by the provider");
|
||
|
}
|
||
|
|
||
|
const res = [];
|
||
|
for (const request of requests) {
|
||
|
const r = await this._retryRequest(request);
|
||
|
res.push(r);
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
protected async _request(request: RPCRequestRaw): Promise<RPCResponse> {
|
||
|
try {
|
||
|
const response = await (
|
||
|
await fetch(this.provider.URL, {
|
||
|
method: "POST",
|
||
|
body: JSON.stringify(request),
|
||
|
})
|
||
|
).json();
|
||
|
|
||
|
if (response.result) {
|
||
|
const tempRequest = {
|
||
|
method: request.method,
|
||
|
params: request.params,
|
||
|
};
|
||
|
const hash = this._pluginApi.util.crypto
|
||
|
.createHash(stringify(tempRequest))
|
||
|
.toString("hex");
|
||
|
this._cache.set(hash, response);
|
||
|
}
|
||
|
return {
|
||
|
success: !response.error,
|
||
|
result: response.error || response.result,
|
||
|
};
|
||
|
} catch (e) {
|
||
|
return {
|
||
|
success: false,
|
||
|
result: { message: `request failed: ${e}` },
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private async _retryRequest(
|
||
|
_request: RPCRequest,
|
||
|
retry = 5
|
||
|
): Promise<RPCResponse> {
|
||
|
const request = {
|
||
|
..._request,
|
||
|
jsonrpc: "2.0",
|
||
|
id: this.generateId(),
|
||
|
};
|
||
|
|
||
|
for (let i = retry; i > 0; i--) {
|
||
|
const res = await this._request(request);
|
||
|
if (res.success) {
|
||
|
return res;
|
||
|
} else if (i == 1) {
|
||
|
console.error(
|
||
|
`RPC batch request failed after maximum retries: ${JSON.stringify(
|
||
|
request,
|
||
|
null,
|
||
|
2
|
||
|
)} ${JSON.stringify(res, null, 2)}`
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
throw new Error("RPC request failed");
|
||
|
}
|
||
|
|
||
|
private generateId(): string {
|
||
|
return Math.floor(Math.random() * 2 ** 64).toFixed();
|
||
|
}
|
||
|
|
||
|
public getCachedRequest(request: RPCRequest): RPCResponse | null {
|
||
|
const hash = this.hashRequest(request);
|
||
|
|
||
|
if (this.cache.has(hash)) {
|
||
|
this.cache.ttl(hash);
|
||
|
return this.cache.get(hash);
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private hashRequest(request: RPCRequest): string {
|
||
|
const tempRequest = {
|
||
|
method: request.method,
|
||
|
params: request.params,
|
||
|
};
|
||
|
|
||
|
return this._pluginApi.util.crypto
|
||
|
.createHash(stringify(tempRequest))
|
||
|
.toString("hex");
|
||
|
}
|
||
|
}
|