@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 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
- params = new URLSearchParams(params).toString();
13
- const response = await fetch(`${this.url}/${method}?${params}`);
14
- return response[type]();
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('blockHashMap');
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 'socket-request-client';
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: <T = any>(event: string, value: T, verbose?: boolean) => void;
8
- subscribe: <T = any>(event: string, handler: import("@vandeurenglenn/little-pubsub").Handler<T>, options?: import("@vandeurenglenn/little-pubsub").SubscribeOptions) => () => void;
7
+ publish: any;
8
+ subscribe: any;
9
9
  };
10
10
  balances(): any;
11
11
  balanceOf(address: string, format: boolean): Promise<any>;
@@ -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: any): Promise<any>;
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>;
@@ -0,0 +1,2 @@
1
+ /** Bound allocation while receiving, not only after arrayBuffer() has buffered an untrusted body. */
2
+ export declare function readBinaryResponse(response: Response, maxBytes?: number): Promise<any>;
@@ -1,13 +1,13 @@
1
- import ClientConnection from 'socket-request-client/connection';
1
+ import BinarySocketClient from './binary-socket.js';
2
2
  export default class Client {
3
3
  url: string;
4
4
  networkVersion: string;
5
- client: ClientConnection;
5
+ client: BinarySocketClient;
6
6
  constructor(url?: string, networkVersion?: string);
7
7
  init(): Promise<void>;
8
8
  get pubsub(): {
9
- publish: (topic: string, value: any) => Promise<any>;
10
- subscribe: (topic: string, cb: any) => Promise<any>;
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: {}): Promise<any>;
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 { SocketRequestClient } from 'socket-request-client';
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
- const client = new SocketRequestClient(this.url, this.networkVersion);
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
- return this.request('staticCall', { contract, method, params });
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.17",
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
- "socket-request-client": "^2.0.9"
60
+ "@leofcoin/binary": "^1.1.0"
60
61
  }
61
62
  }