@leofcoin/endpoint-clients 0.3.17 → 0.4.0
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/exports/http.js +16 -5
- package/exports/index.js +2 -1
- package/exports/transport.js +37 -0
- package/exports/types/binary-socket.d.ts +21 -0
- package/exports/types/direct.d.ts +2 -2
- package/exports/types/http.d.ts +1 -1
- package/exports/types/transport.d.ts +2 -0
- package/exports/types/ws.d.ts +5 -5
- package/exports/ws.js +96 -5
- package/package.json +3 -2
package/exports/http.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readBinaryResponse } from './transport.js';
|
|
2
|
+
import { BINARY_CONTENT_TYPE, encodeWire } from '@leofcoin/binary';
|
|
3
|
+
|
|
1
4
|
class Client {
|
|
2
5
|
url;
|
|
3
6
|
networkVersion;
|
|
@@ -9,9 +12,15 @@ class Client {
|
|
|
9
12
|
this.networkVersion = networkVersion;
|
|
10
13
|
}
|
|
11
14
|
async _fetch(method, params, type = 'json') {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const response = await fetch(`${this.url}/${method}`, {
|
|
16
|
+
method: 'POST', headers: { 'Content-Type': BINARY_CONTENT_TYPE },
|
|
17
|
+
body: encodeWire('rpc', params ?? {}, { maxBytes: 1048576 }),
|
|
18
|
+
signal: AbortSignal.timeout(10000)
|
|
19
|
+
});
|
|
20
|
+
const value = await readBinaryResponse(response);
|
|
21
|
+
if (!response.ok)
|
|
22
|
+
throw new Error(value?.error || `${method} failed with HTTP ${response.status}`);
|
|
23
|
+
return value;
|
|
15
24
|
}
|
|
16
25
|
async _fetchString(method, params) {
|
|
17
26
|
return String(await this._fetch(method, params, 'text'));
|
|
@@ -61,7 +70,9 @@ class Client {
|
|
|
61
70
|
lookup(name) {
|
|
62
71
|
return this._fetch('lookup', { name });
|
|
63
72
|
}
|
|
64
|
-
staticCall(contract, method, params) {
|
|
73
|
+
async staticCall(contract, method, params = []) {
|
|
74
|
+
if (!Array.isArray(params))
|
|
75
|
+
throw new Error('staticCall params must be an array');
|
|
65
76
|
return this._fetch('staticCall', { contract, method, params });
|
|
66
77
|
}
|
|
67
78
|
contracts() {
|
|
@@ -143,7 +154,7 @@ class Client {
|
|
|
143
154
|
return this._fetch('blockHashMap');
|
|
144
155
|
}
|
|
145
156
|
bootstrap() {
|
|
146
|
-
return this._fetch('
|
|
157
|
+
return this._fetch('bootstrap');
|
|
147
158
|
}
|
|
148
159
|
}
|
|
149
160
|
|
package/exports/index.js
CHANGED
|
@@ -2,5 +2,6 @@ export { default as ws } from './ws.js';
|
|
|
2
2
|
export { default as http } from './http.js';
|
|
3
3
|
export { default as direct } from './direct.js';
|
|
4
4
|
export { default as DexClient } from './dex.js';
|
|
5
|
-
import '
|
|
5
|
+
import '@leofcoin/binary';
|
|
6
|
+
import './transport.js';
|
|
6
7
|
import '@leofcoin/utils';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BINARY_CONTENT_TYPE, decodeWire } from '@leofcoin/binary';
|
|
2
|
+
|
|
3
|
+
/** Bound allocation while receiving, not only after arrayBuffer() has buffered an untrusted body. */
|
|
4
|
+
async function readBinaryResponse(response, maxBytes = 4 * 1024 * 1024) {
|
|
5
|
+
if (response.headers.get('content-type')?.split(';')[0] !== BINARY_CONTENT_TYPE)
|
|
6
|
+
throw new Error('Node does not support binary RPC');
|
|
7
|
+
const reader = response.body?.getReader();
|
|
8
|
+
if (!reader)
|
|
9
|
+
throw new Error('Binary response has no body');
|
|
10
|
+
const chunks = [];
|
|
11
|
+
let size = 0;
|
|
12
|
+
try {
|
|
13
|
+
while (true) {
|
|
14
|
+
const { done, value } = await reader.read();
|
|
15
|
+
if (done)
|
|
16
|
+
break;
|
|
17
|
+
size += value.byteLength;
|
|
18
|
+
if (size > maxBytes) {
|
|
19
|
+
await reader.cancel();
|
|
20
|
+
throw new RangeError('Binary response exceeds byte limit');
|
|
21
|
+
}
|
|
22
|
+
chunks.push(value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
reader.releaseLock();
|
|
27
|
+
}
|
|
28
|
+
const bytes = new Uint8Array(size);
|
|
29
|
+
let offset = 0;
|
|
30
|
+
for (const chunk of chunks) {
|
|
31
|
+
bytes.set(chunk, offset);
|
|
32
|
+
offset += chunk.byteLength;
|
|
33
|
+
}
|
|
34
|
+
return decodeWire('rpc', bytes, { maxBytes });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { readBinaryResponse };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default class BinarySocketClient {
|
|
2
|
+
socket: WebSocket;
|
|
3
|
+
pending: Map<string, {
|
|
4
|
+
resolve: Function;
|
|
5
|
+
reject: Function;
|
|
6
|
+
timer: ReturnType<typeof setTimeout>;
|
|
7
|
+
}>;
|
|
8
|
+
listeners: Map<string, Set<Function>>;
|
|
9
|
+
pubsub: {
|
|
10
|
+
publish: (topic: any, value: any) => void;
|
|
11
|
+
subscribe: (topic: any, handler: any) => () => boolean | undefined;
|
|
12
|
+
};
|
|
13
|
+
constructor(url: string, protocol: string);
|
|
14
|
+
init(): Promise<this>;
|
|
15
|
+
fail(error: any): void;
|
|
16
|
+
request({ url, params }: {
|
|
17
|
+
url: any;
|
|
18
|
+
params: any;
|
|
19
|
+
}): Promise<any>;
|
|
20
|
+
close(): void;
|
|
21
|
+
}
|
|
@@ -4,8 +4,8 @@ export default class Client {
|
|
|
4
4
|
client: any;
|
|
5
5
|
constructor(url?: string, networkVersion?: string);
|
|
6
6
|
get pubsub(): {
|
|
7
|
-
publish:
|
|
8
|
-
subscribe:
|
|
7
|
+
publish: any;
|
|
8
|
+
subscribe: any;
|
|
9
9
|
};
|
|
10
10
|
balances(): any;
|
|
11
11
|
balanceOf(address: string, format: boolean): Promise<any>;
|
package/exports/types/http.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export default class Client {
|
|
|
22
22
|
peers(): Promise<any>;
|
|
23
23
|
validators(): Promise<any>;
|
|
24
24
|
lookup(name: any): Promise<any>;
|
|
25
|
-
staticCall(contract: any, method: any, params
|
|
25
|
+
staticCall(contract: any, method: any, params?: never[]): Promise<any>;
|
|
26
26
|
contracts(): Promise<any>;
|
|
27
27
|
totalContracts(): Promise<number>;
|
|
28
28
|
nativeToken(): Promise<string>;
|
package/exports/types/ws.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import BinarySocketClient from './binary-socket.js';
|
|
2
2
|
export default class Client {
|
|
3
3
|
url: string;
|
|
4
4
|
networkVersion: string;
|
|
5
|
-
client:
|
|
5
|
+
client: BinarySocketClient;
|
|
6
6
|
constructor(url?: string, networkVersion?: string);
|
|
7
7
|
init(): Promise<void>;
|
|
8
8
|
get pubsub(): {
|
|
9
|
-
publish: (topic:
|
|
10
|
-
subscribe: (topic:
|
|
9
|
+
publish: (topic: any, value: any) => void;
|
|
10
|
+
subscribe: (topic: any, handler: any) => () => boolean | undefined;
|
|
11
11
|
};
|
|
12
12
|
request(url: string, params?: {}): Promise<any>;
|
|
13
13
|
balances(): Promise<any>;
|
|
@@ -23,7 +23,7 @@ export default class Client {
|
|
|
23
23
|
peers(): Promise<any>;
|
|
24
24
|
validators(): Promise<any>;
|
|
25
25
|
lookup(name: string): Promise<any>;
|
|
26
|
-
staticCall(contract: string, method: string, params
|
|
26
|
+
staticCall(contract: string, method: string, params?: any[]): Promise<any>;
|
|
27
27
|
contracts(): Promise<number>;
|
|
28
28
|
totalContracts(): Promise<number>;
|
|
29
29
|
nativeToken(): Promise<string>;
|
package/exports/ws.js
CHANGED
|
@@ -1,4 +1,81 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeWire, encodeWire } from '@leofcoin/binary';
|
|
2
|
+
|
|
3
|
+
class BinarySocketClient {
|
|
4
|
+
socket;
|
|
5
|
+
pending = new Map();
|
|
6
|
+
listeners = new Map();
|
|
7
|
+
pubsub = {
|
|
8
|
+
publish: (topic, value) => { for (const handler of this.listeners.get(topic) || [])
|
|
9
|
+
handler(value); },
|
|
10
|
+
subscribe: (topic, handler) => {
|
|
11
|
+
if (!this.listeners.has(topic))
|
|
12
|
+
this.listeners.set(topic, new Set());
|
|
13
|
+
this.listeners.get(topic).add(handler);
|
|
14
|
+
return () => this.listeners.get(topic)?.delete(handler);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
constructor(url, protocol) {
|
|
18
|
+
this.socket = new WebSocket(url, `${protocol}.lfc-binary-v1`);
|
|
19
|
+
this.socket.binaryType = 'arraybuffer';
|
|
20
|
+
}
|
|
21
|
+
async init() {
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
const timer = setTimeout(() => { this.socket.close(); reject(new Error('Binary RPC connection timed out')); }, 10000);
|
|
24
|
+
this.socket.addEventListener('open', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
25
|
+
this.socket.addEventListener('error', () => { clearTimeout(timer); reject(new Error('Binary RPC connection failed')); }, { once: true });
|
|
26
|
+
});
|
|
27
|
+
this.socket.addEventListener('message', event => {
|
|
28
|
+
try {
|
|
29
|
+
if (!(event.data instanceof ArrayBuffer))
|
|
30
|
+
throw new Error('Binary RPC frames required');
|
|
31
|
+
const message = decodeWire('rpc-response', new Uint8Array(event.data), { maxBytes: 4 * 1024 * 1024 });
|
|
32
|
+
if (!message || typeof message.id !== 'string' || !Number.isInteger(message.status))
|
|
33
|
+
throw new Error('Invalid RPC response');
|
|
34
|
+
const pending = this.pending.get(message.id);
|
|
35
|
+
if (!pending)
|
|
36
|
+
return;
|
|
37
|
+
clearTimeout(pending.timer);
|
|
38
|
+
this.pending.delete(message.id);
|
|
39
|
+
if (message.status >= 400)
|
|
40
|
+
pending.reject(new Error(message.value?.error || `RPC failed (${message.status})`));
|
|
41
|
+
else
|
|
42
|
+
pending.resolve(message.value);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
this.fail(error);
|
|
46
|
+
this.socket.close(1002, 'Invalid binary RPC');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
this.socket.addEventListener('close', () => this.fail(new Error('Binary RPC connection closed')));
|
|
50
|
+
this.socket.addEventListener('error', () => this.fail(new Error('Binary RPC connection failed')));
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
fail(error) { for (const { reject, timer } of this.pending.values()) {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
reject(error);
|
|
56
|
+
} this.pending.clear(); }
|
|
57
|
+
request({ url, params }) {
|
|
58
|
+
if (this.socket.readyState !== WebSocket.OPEN)
|
|
59
|
+
return Promise.reject(new Error('Binary RPC is not connected'));
|
|
60
|
+
if (this.pending.size >= 32)
|
|
61
|
+
return Promise.reject(new Error('Too many pending RPC requests'));
|
|
62
|
+
const id = crypto.randomUUID();
|
|
63
|
+
const bytes = encodeWire('rpc-request', { id, url, params }, { maxBytes: 1048576 });
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const timer = setTimeout(() => { this.pending.delete(id); reject(new Error('Binary RPC request timed out')); }, 10000);
|
|
66
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
67
|
+
try {
|
|
68
|
+
this.socket.send(bytes);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
this.pending.delete(id);
|
|
73
|
+
reject(error);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
close() { this.fail(new Error('Binary RPC client closed')); this.socket.close(); }
|
|
78
|
+
}
|
|
2
79
|
|
|
3
80
|
class Client {
|
|
4
81
|
url;
|
|
@@ -9,8 +86,7 @@ class Client {
|
|
|
9
86
|
this.networkVersion = networkVersion;
|
|
10
87
|
}
|
|
11
88
|
async init() {
|
|
12
|
-
|
|
13
|
-
this.client = (await client.init());
|
|
89
|
+
this.client = await new BinarySocketClient(this.url, this.networkVersion).init();
|
|
14
90
|
}
|
|
15
91
|
get pubsub() {
|
|
16
92
|
return {
|
|
@@ -60,8 +136,23 @@ class Client {
|
|
|
60
136
|
lookup(name) {
|
|
61
137
|
return this.request('lookup', { name });
|
|
62
138
|
}
|
|
63
|
-
staticCall(contract, method, params) {
|
|
64
|
-
|
|
139
|
+
async staticCall(contract, method, params = []) {
|
|
140
|
+
if (!Array.isArray(params))
|
|
141
|
+
throw new Error('staticCall params must be an array');
|
|
142
|
+
try {
|
|
143
|
+
const value = await this.request('staticCall', {
|
|
144
|
+
contract, method, params
|
|
145
|
+
});
|
|
146
|
+
if (value?.error)
|
|
147
|
+
throw new Error(value.error);
|
|
148
|
+
return value ?? null;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (error instanceof Error)
|
|
152
|
+
throw error;
|
|
153
|
+
const message = error && typeof error === 'object' && 'error' in error ? error.error : undefined;
|
|
154
|
+
throw new Error(typeof error === 'string' ? error : typeof message === 'string' ? message : 'Read-only RPC query failed');
|
|
155
|
+
}
|
|
65
156
|
}
|
|
66
157
|
contracts() {
|
|
67
158
|
return this.request('contracts');
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/endpoint-clients",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"exports": {
|
|
6
|
+
"./transport": { "import": "./exports/transport.js", "types": "./exports/types/transport.d.ts" },
|
|
6
7
|
".": {
|
|
7
8
|
"import": "./exports/index.js",
|
|
8
9
|
"types": "./exports/types/index.d.ts"
|
|
@@ -56,6 +57,6 @@
|
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
59
|
"@leofcoin/utils": "^1.1.45",
|
|
59
|
-
"
|
|
60
|
+
"@leofcoin/binary": "^1.1.0"
|
|
60
61
|
}
|
|
61
62
|
}
|