@peerbit/rpc 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +65 -0
- package/lib/esm/controller.d.ts +62 -0
- package/lib/esm/controller.js +266 -0
- package/lib/esm/controller.js.map +1 -0
- package/lib/esm/encoding.d.ts +19 -0
- package/lib/esm/encoding.js +59 -0
- package/lib/esm/encoding.js.map +1 -0
- package/lib/esm/index.d.ts +4 -0
- package/lib/esm/index.js +5 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/io.d.ts +22 -0
- package/lib/esm/io.js +3 -0
- package/lib/esm/io.js.map +1 -0
- package/lib/esm/package.json +3 -0
- package/lib/esm/utils.d.ts +6 -0
- package/lib/esm/utils.js +57 -0
- package/lib/esm/utils.js.map +1 -0
- package/package.json +45 -0
- package/src/controller.ts +413 -0
- package/src/encoding.ts +41 -0
- package/src/index.ts +4 -0
- package/src/io.ts +28 -0
- package/src/utils.ts +73 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { field, fixedArray, option, variant } from "@dao-xyz/borsh";
|
|
11
|
+
import { MaybeEncrypted, X25519PublicKey } from "@peerbit/crypto";
|
|
12
|
+
export let RPCMessage = class RPCMessage {
|
|
13
|
+
};
|
|
14
|
+
RPCMessage = __decorate([
|
|
15
|
+
variant(0)
|
|
16
|
+
], RPCMessage);
|
|
17
|
+
export let RequestV0 = class RequestV0 extends RPCMessage {
|
|
18
|
+
respondTo;
|
|
19
|
+
request;
|
|
20
|
+
constructor(properties) {
|
|
21
|
+
super();
|
|
22
|
+
this.respondTo = properties.respondTo;
|
|
23
|
+
this.request = properties.request;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
__decorate([
|
|
27
|
+
field({ type: option(X25519PublicKey) }),
|
|
28
|
+
__metadata("design:type", X25519PublicKey)
|
|
29
|
+
], RequestV0.prototype, "respondTo", void 0);
|
|
30
|
+
__decorate([
|
|
31
|
+
field({ type: MaybeEncrypted }),
|
|
32
|
+
__metadata("design:type", MaybeEncrypted)
|
|
33
|
+
], RequestV0.prototype, "request", void 0);
|
|
34
|
+
RequestV0 = __decorate([
|
|
35
|
+
variant(0),
|
|
36
|
+
__metadata("design:paramtypes", [Object])
|
|
37
|
+
], RequestV0);
|
|
38
|
+
export let ResponseV0 = class ResponseV0 extends RPCMessage {
|
|
39
|
+
requestId;
|
|
40
|
+
response;
|
|
41
|
+
constructor(properties) {
|
|
42
|
+
super();
|
|
43
|
+
this.response = properties.response;
|
|
44
|
+
this.requestId = properties.requestId;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
__decorate([
|
|
48
|
+
field({ type: fixedArray("u8", 32) }),
|
|
49
|
+
__metadata("design:type", Uint8Array)
|
|
50
|
+
], ResponseV0.prototype, "requestId", void 0);
|
|
51
|
+
__decorate([
|
|
52
|
+
field({ type: MaybeEncrypted }),
|
|
53
|
+
__metadata("design:type", MaybeEncrypted)
|
|
54
|
+
], ResponseV0.prototype, "response", void 0);
|
|
55
|
+
ResponseV0 = __decorate([
|
|
56
|
+
variant(1),
|
|
57
|
+
__metadata("design:paramtypes", [Object])
|
|
58
|
+
], ResponseV0);
|
|
59
|
+
//# sourceMappingURL=encoding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.js","sourceRoot":"","sources":["../../src/encoding.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAG3D,WAAe,UAAU,GAAzB,MAAe,UAAU;CAAG,CAAA;AAAb,UAAU;IAD/B,OAAO,CAAC,CAAC,CAAC;GACW,UAAU,CAAG;AAG5B,WAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,UAAU;IAExC,SAAS,CAAmB;IAG5B,OAAO,CAAsB;IAE7B,YAAY,UAGX;QACA,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC;QACtC,IAAI,CAAC,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC;IACnC,CAAC;CACD,CAAA;AAbA;IADC,KAAK,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,eAAe,CAAC,EAAE,CAAC;8BAC7B,eAAe;4CAAC;AAG5B;IADC,KAAK,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;8BACvB,cAAc;0CAAM;AALjB,SAAS;IADrB,OAAO,CAAC,CAAC,CAAC;;GACE,SAAS,CAerB;AAGM,WAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,UAAU;IAEzC,SAAS,CAAa;IAGtB,QAAQ,CAAsB;IAE9B,YAAY,UAGX;QACA,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC;QACpC,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC;IACvC,CAAC;CACD,CAAA;AAbA;IADC,KAAK,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;8BAC3B,UAAU;6CAAC;AAGtB;IADC,KAAK,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;8BACtB,cAAc;4CAAM;AALlB,UAAU;IADtB,OAAO,CAAC,CAAC,CAAC;;GACE,UAAU,CAetB"}
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC"}
|
package/lib/esm/io.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { X25519PublicKey, Ed25519PublicKey, PublicSignKey, X25519Keypair } from "@peerbit/crypto";
|
|
2
|
+
export declare const logger: import("pino").Logger<import("pino").LoggerOptions | import("pino").DestinationStream>;
|
|
3
|
+
export type RPCOptions<R> = {
|
|
4
|
+
amount?: number;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
isTrusted?: (publicKey: PublicSignKey) => Promise<boolean>;
|
|
7
|
+
strict?: boolean;
|
|
8
|
+
onResponse?: (response: R, from?: PublicSignKey) => void;
|
|
9
|
+
stopper?: (stopper: () => void) => void;
|
|
10
|
+
} & PublishOptions;
|
|
11
|
+
export type PublishOptions = {
|
|
12
|
+
encryption?: {
|
|
13
|
+
key: X25519Keypair;
|
|
14
|
+
responders?: (X25519PublicKey | Ed25519PublicKey)[];
|
|
15
|
+
};
|
|
16
|
+
to?: PublicSignKey[] | string[];
|
|
17
|
+
strict?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type RPCResponse<R> = {
|
|
20
|
+
response: R;
|
|
21
|
+
from?: PublicSignKey;
|
|
22
|
+
};
|
package/lib/esm/io.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"io.js","sourceRoot":"","sources":["../../src/io.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,MAAM,IAAI,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,CAAC,MAAM,MAAM,GAAG,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { RPC } from "./controller";
|
|
2
|
+
import { RPCOptions, RPCResponse } from "./io";
|
|
3
|
+
export declare class MissingResponsesError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare const queryAll: <Q, R>(rpc: RPC<Q, R>, groups: string[][], request: Q, responseHandler: (response: RPCResponse<R>[]) => Promise<void> | void, options?: RPCOptions<R> | undefined) => Promise<void>;
|
package/lib/esm/utils.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export class MissingResponsesError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export const queryAll = (rpc, groups, request, responseHandler, options) => {
|
|
7
|
+
// In each shard/group only query a subset
|
|
8
|
+
groups = [...groups].filter((x) => !x.find((y) => y === rpc.node.identity.publicKey.hashcode()));
|
|
9
|
+
let rng = Math.round(Math.random() * groups.length);
|
|
10
|
+
const startRng = rng;
|
|
11
|
+
const fn = async () => {
|
|
12
|
+
let missingReponses = false;
|
|
13
|
+
while (groups.length > 0) {
|
|
14
|
+
const peersToQuery = new Array(groups.length);
|
|
15
|
+
let counter = 0;
|
|
16
|
+
const peerToGroupIndex = new Map();
|
|
17
|
+
for (let i = 0; i < groups.length; i++) {
|
|
18
|
+
const group = groups[i];
|
|
19
|
+
peersToQuery[counter] = group[rng % group.length];
|
|
20
|
+
peerToGroupIndex.set(peersToQuery[counter], i);
|
|
21
|
+
counter++;
|
|
22
|
+
}
|
|
23
|
+
if (peersToQuery.length > 0) {
|
|
24
|
+
const results = await rpc.request(request, {
|
|
25
|
+
...options,
|
|
26
|
+
to: peersToQuery,
|
|
27
|
+
});
|
|
28
|
+
for (const result of results) {
|
|
29
|
+
if (!result.from) {
|
|
30
|
+
throw new Error("Unexpected, missing from");
|
|
31
|
+
}
|
|
32
|
+
peerToGroupIndex.delete(result.from.hashcode());
|
|
33
|
+
}
|
|
34
|
+
await responseHandler(results);
|
|
35
|
+
const indicesLeft = new Set([...peerToGroupIndex.values()]);
|
|
36
|
+
rng += 1;
|
|
37
|
+
groups = groups.filter((v, ix) => {
|
|
38
|
+
if (indicesLeft.has(ix)) {
|
|
39
|
+
const peerIndex = rng % v.length;
|
|
40
|
+
if (rng === startRng || peerIndex === startRng % v.length) {
|
|
41
|
+
// TODO Last condition needed?
|
|
42
|
+
missingReponses = true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (missingReponses) {
|
|
52
|
+
throw new MissingResponsesError("Did not recieve responses from all shards");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return fn();
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@peerbit/rpc",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "RPC calls for peers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"module": "lib/esm/index.js",
|
|
8
|
+
"types": "lib/esm/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
"import": "./lib/esm/index.js",
|
|
11
|
+
"require": "./lib/cjs/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"lib",
|
|
15
|
+
"src",
|
|
16
|
+
"!src/**/__tests__",
|
|
17
|
+
"!lib/**/__tests__",
|
|
18
|
+
"!documentation",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "shx rm -rf lib/*",
|
|
26
|
+
"build": "yarn clean && tsc -p tsconfig.json",
|
|
27
|
+
"postbuild": "echo '{\"type\":\"module\"} ' | node ../../../node_modules/.bin/json > lib/esm/package.json",
|
|
28
|
+
"test": "node ../../../node_modules/.bin/jest test -c ../../../jest.config.ts --runInBand --forceExit",
|
|
29
|
+
"test:unit": "node ../../../node_modules/.bin/jest test -c ../../../jest.config.unit.ts --runInBand --forceExit",
|
|
30
|
+
"test:integration": "node ../node_modules/.bin/jest test -c ../../../jest.config.integration.ts --runInBand --forceExit"
|
|
31
|
+
},
|
|
32
|
+
"author": "dao.xyz",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@dao-xyz/borsh": "^5.1.5",
|
|
36
|
+
"@peerbit/crypto": "1.0.1",
|
|
37
|
+
"@peerbit/logger": "1.0.0",
|
|
38
|
+
"@peerbit/program": "1.0.1",
|
|
39
|
+
"@peerbit/time": "1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@peerbit/test-utils": "^1.0.2"
|
|
43
|
+
},
|
|
44
|
+
"gitHead": "595db9f1efebf604393eddfff5f678f5d8f16142"
|
|
45
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbstractType,
|
|
3
|
+
BorshError,
|
|
4
|
+
deserialize,
|
|
5
|
+
serialize,
|
|
6
|
+
variant,
|
|
7
|
+
} from "@dao-xyz/borsh";
|
|
8
|
+
import {
|
|
9
|
+
DecryptedThing,
|
|
10
|
+
MaybeEncrypted,
|
|
11
|
+
PublicSignKey,
|
|
12
|
+
toBase64,
|
|
13
|
+
AccessError,
|
|
14
|
+
X25519PublicKey,
|
|
15
|
+
X25519Keypair,
|
|
16
|
+
} from "@peerbit/crypto";
|
|
17
|
+
import { RequestV0, ResponseV0, RPCMessage } from "./encoding.js";
|
|
18
|
+
import { RPCOptions, logger, RPCResponse, PublishOptions } from "./io.js";
|
|
19
|
+
import { AbstractProgram, Address } from "@peerbit/program";
|
|
20
|
+
import {
|
|
21
|
+
PubSubData,
|
|
22
|
+
PublishOptions as PubSubPublishOptions,
|
|
23
|
+
} from "@peerbit/pubsub-interface";
|
|
24
|
+
import { ComposableProgram } from "@peerbit/program";
|
|
25
|
+
import { DataMessage } from "@peerbit/stream-interface";
|
|
26
|
+
import pDefer, { DeferredPromise } from "p-defer";
|
|
27
|
+
import { waitFor } from "@peerbit/time";
|
|
28
|
+
|
|
29
|
+
export type SearchContext = (() => Address) | AbstractProgram;
|
|
30
|
+
export type CanRead = (key?: PublicSignKey) => Promise<boolean> | boolean;
|
|
31
|
+
|
|
32
|
+
export type RPCSetupOptions<Q, R> = {
|
|
33
|
+
topic: string;
|
|
34
|
+
queryType: AbstractType<Q>;
|
|
35
|
+
responseType: AbstractType<R>;
|
|
36
|
+
canRead?: CanRead;
|
|
37
|
+
responseHandler?: ResponseHandler<Q, R>;
|
|
38
|
+
subscriptionData?: Uint8Array;
|
|
39
|
+
};
|
|
40
|
+
export type QueryContext = {
|
|
41
|
+
from?: PublicSignKey;
|
|
42
|
+
address: string;
|
|
43
|
+
};
|
|
44
|
+
export type ResponseHandler<Q, R> = (
|
|
45
|
+
query: Q,
|
|
46
|
+
context: QueryContext
|
|
47
|
+
) => Promise<R | undefined> | R | undefined;
|
|
48
|
+
|
|
49
|
+
const createValueResolver = <T>(
|
|
50
|
+
type: AbstractType<T> | Uint8Array
|
|
51
|
+
): ((decryptedThings: DecryptedThing<T>) => T) => {
|
|
52
|
+
if ((type as any) === Uint8Array) {
|
|
53
|
+
return (decrypted) => decrypted._data as T;
|
|
54
|
+
} else {
|
|
55
|
+
return (decrypted) => decrypted.getValue(type as AbstractType<T>);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
@variant("rpc")
|
|
60
|
+
export class RPC<Q, R> extends ComposableProgram<RPCSetupOptions<Q, R>> {
|
|
61
|
+
canRead: CanRead;
|
|
62
|
+
|
|
63
|
+
private _subscribed = false;
|
|
64
|
+
private _responseHandler?: ResponseHandler<Q, (R | undefined) | R>;
|
|
65
|
+
private _responseResolver: Map<
|
|
66
|
+
string,
|
|
67
|
+
(properties: { response: ResponseV0; message: DataMessage }) => any
|
|
68
|
+
>;
|
|
69
|
+
private _requestType: AbstractType<Q> | Uint8ArrayConstructor;
|
|
70
|
+
private _responseType: AbstractType<R>;
|
|
71
|
+
private _rpcTopic: string | undefined;
|
|
72
|
+
private _onMessageBinded: ((arg: any) => any) | undefined = undefined;
|
|
73
|
+
private _subscriptionMetaData: Uint8Array | undefined;
|
|
74
|
+
|
|
75
|
+
private _keypair: X25519Keypair;
|
|
76
|
+
|
|
77
|
+
private _getResponseValueFn: (decrypted: DecryptedThing<R>) => R;
|
|
78
|
+
private _getRequestValueFn: (decrypted: DecryptedThing<Q>) => Q;
|
|
79
|
+
|
|
80
|
+
async open(args: RPCSetupOptions<Q, R>): Promise<void> {
|
|
81
|
+
this._rpcTopic = args.topic ?? this._rpcTopic;
|
|
82
|
+
this._responseHandler = args.responseHandler;
|
|
83
|
+
this._requestType = args.queryType;
|
|
84
|
+
this._responseType = args.responseType;
|
|
85
|
+
this._responseResolver = new Map();
|
|
86
|
+
this._subscriptionMetaData = args.subscriptionData;
|
|
87
|
+
this.canRead = args.canRead || (() => Promise.resolve(true));
|
|
88
|
+
|
|
89
|
+
this._getResponseValueFn = createValueResolver(this._responseType);
|
|
90
|
+
this._getRequestValueFn = createValueResolver(this._requestType);
|
|
91
|
+
|
|
92
|
+
this._keypair = await X25519Keypair.create();
|
|
93
|
+
await this._subscribe();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async close(from?: AbstractProgram): Promise<boolean> {
|
|
97
|
+
if (this._subscribed) {
|
|
98
|
+
await this.node.services.pubsub.unsubscribe(this.rpcTopic);
|
|
99
|
+
await this.node.services.pubsub.removeEventListener(
|
|
100
|
+
"data",
|
|
101
|
+
this._onMessage
|
|
102
|
+
);
|
|
103
|
+
this._subscribed = false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return super.close(from);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private _subscribing: Promise<void>;
|
|
110
|
+
private async _subscribe(): Promise<void> {
|
|
111
|
+
await this._subscribing;
|
|
112
|
+
if (this._subscribed) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this._subscribed = true;
|
|
116
|
+
this._onMessageBinded = this._onMessageBinded || this._onMessage.bind(this);
|
|
117
|
+
this._subscribing = this.node.services.pubsub
|
|
118
|
+
.subscribe(this.rpcTopic, { data: this._subscriptionMetaData })
|
|
119
|
+
.then(() => {
|
|
120
|
+
this.node.services.pubsub.addEventListener(
|
|
121
|
+
"data",
|
|
122
|
+
this._onMessageBinded!
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await this._subscribing;
|
|
127
|
+
await this.node.services.pubsub.requestSubscribers(this.rpcTopic);
|
|
128
|
+
|
|
129
|
+
logger.debug("subscribing to query topic (responses): " + this.rpcTopic);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async _onMessage(
|
|
133
|
+
evt: CustomEvent<{ data: PubSubData; message: DataMessage }>
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const { data, message } = evt.detail;
|
|
136
|
+
|
|
137
|
+
if (data?.topics.find((x) => x === this.rpcTopic) != null) {
|
|
138
|
+
try {
|
|
139
|
+
const rpcMessage = deserialize(data.data, RPCMessage);
|
|
140
|
+
if (rpcMessage instanceof RequestV0) {
|
|
141
|
+
if (this._responseHandler) {
|
|
142
|
+
const maybeEncrypted = rpcMessage.request;
|
|
143
|
+
const decrypted = await maybeEncrypted.decrypt(this.node.keychain);
|
|
144
|
+
|
|
145
|
+
if (!(await this.canRead(message.sender))) {
|
|
146
|
+
throw new AccessError();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const response = await this._responseHandler(
|
|
150
|
+
this._getRequestValueFn(decrypted),
|
|
151
|
+
{
|
|
152
|
+
address: this.rpcTopic,
|
|
153
|
+
from: message.sender,
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (response && rpcMessage.respondTo) {
|
|
158
|
+
// send query and wait for replies in a generator like behaviour
|
|
159
|
+
const serializedResponse = serialize(response);
|
|
160
|
+
|
|
161
|
+
// we use the peerId/libp2p identity for signatures, since we want to be able to send a message
|
|
162
|
+
// with pubsub with a certain reciever. If we use (this.identity) we are going to use an identity
|
|
163
|
+
// that is now known in the .pubsub network, hence the message might not be delivired if we
|
|
164
|
+
// send with { to: [RECIEVER] } param
|
|
165
|
+
|
|
166
|
+
const decryptedMessage = new DecryptedThing<Uint8Array>({
|
|
167
|
+
data: serializedResponse,
|
|
168
|
+
});
|
|
169
|
+
let maybeEncryptedMessage: MaybeEncrypted<Uint8Array> =
|
|
170
|
+
decryptedMessage;
|
|
171
|
+
|
|
172
|
+
maybeEncryptedMessage = await decryptedMessage.encrypt(
|
|
173
|
+
this._keypair,
|
|
174
|
+
rpcMessage.respondTo
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
await this.node.services.pubsub.publish(
|
|
178
|
+
serialize(
|
|
179
|
+
new ResponseV0({
|
|
180
|
+
response: maybeEncryptedMessage,
|
|
181
|
+
requestId: message.id,
|
|
182
|
+
})
|
|
183
|
+
),
|
|
184
|
+
{
|
|
185
|
+
topics: [this.rpcTopic],
|
|
186
|
+
to: [message.sender],
|
|
187
|
+
strict: true,
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else if (rpcMessage instanceof ResponseV0) {
|
|
193
|
+
const id = toBase64(rpcMessage.requestId);
|
|
194
|
+
let handler = this._responseResolver.get(id);
|
|
195
|
+
if (!handler) {
|
|
196
|
+
handler = await waitFor(() => this._responseResolver.get(id));
|
|
197
|
+
}
|
|
198
|
+
handler!({
|
|
199
|
+
message,
|
|
200
|
+
response: rpcMessage,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
} catch (error: any) {
|
|
204
|
+
if (error instanceof AccessError) {
|
|
205
|
+
logger.debug("Got message I could not decrypt");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (error instanceof BorshError) {
|
|
210
|
+
logger.error("Got message for a different namespace");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
logger.error(
|
|
214
|
+
"Error handling query: " +
|
|
215
|
+
(error?.message ? error?.message?.toString() : error)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async seal(
|
|
222
|
+
request: Q,
|
|
223
|
+
respondTo?: X25519PublicKey,
|
|
224
|
+
options?: PublishOptions
|
|
225
|
+
) {
|
|
226
|
+
const requestData =
|
|
227
|
+
(this._requestType as any) === Uint8Array
|
|
228
|
+
? (request as Uint8Array)
|
|
229
|
+
: serialize(request);
|
|
230
|
+
|
|
231
|
+
const decryptedMessage = new DecryptedThing<Uint8Array>({
|
|
232
|
+
data: requestData,
|
|
233
|
+
});
|
|
234
|
+
let maybeEncryptedMessage: MaybeEncrypted<Uint8Array> = decryptedMessage;
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
options?.encryption?.responders &&
|
|
238
|
+
options?.encryption?.responders.length > 0
|
|
239
|
+
) {
|
|
240
|
+
maybeEncryptedMessage = await decryptedMessage.encrypt(
|
|
241
|
+
options.encryption.key,
|
|
242
|
+
...options.encryption.responders
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const requestMessage = new RequestV0({
|
|
247
|
+
request: maybeEncryptedMessage,
|
|
248
|
+
respondTo,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return requestMessage;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private getPublishOptions(options?: PublishOptions): PubSubPublishOptions {
|
|
255
|
+
return options?.to
|
|
256
|
+
? { to: options.to, strict: true, topics: [this.rpcTopic] }
|
|
257
|
+
: { topics: [this.rpcTopic] };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Send message and don't expect any response
|
|
262
|
+
* @param message
|
|
263
|
+
* @param options
|
|
264
|
+
*/
|
|
265
|
+
public async send(message: Q, options?: PublishOptions): Promise<void> {
|
|
266
|
+
await this.node.services.pubsub.publish(
|
|
267
|
+
serialize(await this.seal(message, undefined, options)),
|
|
268
|
+
this.getPublishOptions(options)
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private createResponseHandler(
|
|
273
|
+
promise: DeferredPromise<any>,
|
|
274
|
+
keypair: X25519Keypair,
|
|
275
|
+
allResults: RPCResponse<R>[],
|
|
276
|
+
responders: Set<string>,
|
|
277
|
+
expectedResponders?: Set<string>,
|
|
278
|
+
options?: RPCOptions<R>
|
|
279
|
+
) {
|
|
280
|
+
return async (properties: {
|
|
281
|
+
response: ResponseV0;
|
|
282
|
+
message: DataMessage;
|
|
283
|
+
}) => {
|
|
284
|
+
try {
|
|
285
|
+
const { response, message } = properties;
|
|
286
|
+
const from = message.sender;
|
|
287
|
+
|
|
288
|
+
if (options?.isTrusted && !(await options?.isTrusted(from))) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const maybeEncrypted = response.response;
|
|
293
|
+
const decrypted = await maybeEncrypted.decrypt(keypair);
|
|
294
|
+
const resultData = this._getResponseValueFn(decrypted);
|
|
295
|
+
|
|
296
|
+
if (expectedResponders) {
|
|
297
|
+
if (from && expectedResponders?.has(from.hashcode())) {
|
|
298
|
+
options?.onResponse && options?.onResponse(resultData, from);
|
|
299
|
+
allResults.push({ response: resultData, from });
|
|
300
|
+
responders.add(from.hashcode());
|
|
301
|
+
if (responders.size === expectedResponders.size) {
|
|
302
|
+
promise.resolve();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
options?.onResponse && options?.onResponse(resultData, from);
|
|
307
|
+
allResults.push({ response: resultData, from });
|
|
308
|
+
if (
|
|
309
|
+
options?.amount != null &&
|
|
310
|
+
allResults.length >= (options?.amount as number)
|
|
311
|
+
) {
|
|
312
|
+
promise.resolve();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (error instanceof AccessError) {
|
|
317
|
+
return; // Ignore things we can not open
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (error instanceof BorshError && !options?.strict) {
|
|
321
|
+
logger.debug("Namespace error");
|
|
322
|
+
return; // Name space conflict most likely
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.error("failed ot deserialize query response", error);
|
|
326
|
+
promise.reject(error);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Send a request and expect a response
|
|
333
|
+
* @param request
|
|
334
|
+
* @param options
|
|
335
|
+
* @returns
|
|
336
|
+
*/
|
|
337
|
+
public async request(
|
|
338
|
+
request: Q,
|
|
339
|
+
options?: RPCOptions<R>
|
|
340
|
+
): Promise<RPCResponse<R>[]> {
|
|
341
|
+
// We are generatinga new encryption keypair for each send, so we now that when we get the responses, they are encrypted specifcally for me, and for this request
|
|
342
|
+
// this allows us to easily disregard a bunch of message just beacuse they are for a different reciever!
|
|
343
|
+
const keypair = await X25519Keypair.create();
|
|
344
|
+
|
|
345
|
+
// send query and wait for replies in a generator like behaviour
|
|
346
|
+
let timeoutFn: any = undefined;
|
|
347
|
+
|
|
348
|
+
const requestMessage = await this.seal(request, keypair.publicKey, options);
|
|
349
|
+
const requestBytes = serialize(requestMessage);
|
|
350
|
+
|
|
351
|
+
const allResults: RPCResponse<R>[] = [];
|
|
352
|
+
|
|
353
|
+
const deferredPromise = pDefer();
|
|
354
|
+
options?.stopper && options.stopper(deferredPromise.resolve);
|
|
355
|
+
timeoutFn = setTimeout(() => {
|
|
356
|
+
deferredPromise.resolve();
|
|
357
|
+
}, options?.timeout || 10 * 1000);
|
|
358
|
+
|
|
359
|
+
const expectedResponders =
|
|
360
|
+
options?.to && options.to.length > 0
|
|
361
|
+
? new Set(
|
|
362
|
+
options.to.map((x) => (typeof x === "string" ? x : x.hashcode()))
|
|
363
|
+
)
|
|
364
|
+
: undefined;
|
|
365
|
+
|
|
366
|
+
const responders = new Set<string>();
|
|
367
|
+
|
|
368
|
+
const id = toBase64(
|
|
369
|
+
await this.node.services.pubsub.publish(
|
|
370
|
+
requestBytes,
|
|
371
|
+
this.getPublishOptions(options)
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
this._responseResolver.set(
|
|
375
|
+
id,
|
|
376
|
+
this.createResponseHandler(
|
|
377
|
+
deferredPromise,
|
|
378
|
+
keypair,
|
|
379
|
+
allResults,
|
|
380
|
+
responders,
|
|
381
|
+
expectedResponders,
|
|
382
|
+
options
|
|
383
|
+
)
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
await deferredPromise.promise;
|
|
388
|
+
} catch (error: any) {
|
|
389
|
+
// timeout
|
|
390
|
+
if (error.constructor.name != "TimeoutError") {
|
|
391
|
+
throw new Error(
|
|
392
|
+
"Got unexpected error when query: " + error.constructor.name
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
} finally {
|
|
396
|
+
clearTimeout(timeoutFn);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
this._responseResolver.delete(id);
|
|
400
|
+
return allResults;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
public get rpcTopic(): string {
|
|
404
|
+
if (!this._rpcTopic) {
|
|
405
|
+
throw new Error("Not initialized");
|
|
406
|
+
}
|
|
407
|
+
return this._rpcTopic;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
getTopics(): string[] {
|
|
411
|
+
return [this.rpcTopic];
|
|
412
|
+
}
|
|
413
|
+
}
|
package/src/encoding.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { field, fixedArray, option, variant } from "@dao-xyz/borsh";
|
|
2
|
+
import { MaybeEncrypted, X25519PublicKey } from "@peerbit/crypto";
|
|
3
|
+
|
|
4
|
+
@variant(0)
|
|
5
|
+
export abstract class RPCMessage {}
|
|
6
|
+
|
|
7
|
+
@variant(0)
|
|
8
|
+
export class RequestV0 extends RPCMessage {
|
|
9
|
+
@field({ type: option(X25519PublicKey) })
|
|
10
|
+
respondTo?: X25519PublicKey;
|
|
11
|
+
|
|
12
|
+
@field({ type: MaybeEncrypted })
|
|
13
|
+
request: MaybeEncrypted<any>;
|
|
14
|
+
|
|
15
|
+
constructor(properties: {
|
|
16
|
+
request: MaybeEncrypted<any>;
|
|
17
|
+
respondTo?: X25519PublicKey;
|
|
18
|
+
}) {
|
|
19
|
+
super();
|
|
20
|
+
this.respondTo = properties.respondTo;
|
|
21
|
+
this.request = properties.request;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@variant(1)
|
|
26
|
+
export class ResponseV0 extends RPCMessage {
|
|
27
|
+
@field({ type: fixedArray("u8", 32) })
|
|
28
|
+
requestId: Uint8Array;
|
|
29
|
+
|
|
30
|
+
@field({ type: MaybeEncrypted })
|
|
31
|
+
response: MaybeEncrypted<any>;
|
|
32
|
+
|
|
33
|
+
constructor(properties: {
|
|
34
|
+
response: MaybeEncrypted<any>;
|
|
35
|
+
requestId: Uint8Array;
|
|
36
|
+
}) {
|
|
37
|
+
super();
|
|
38
|
+
this.response = properties.response;
|
|
39
|
+
this.requestId = properties.requestId;
|
|
40
|
+
}
|
|
41
|
+
}
|