@leofcoin/endpoint-clients 0.3.16 → 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/dex.js ADDED
@@ -0,0 +1,137 @@
1
+ const BASIS_POINTS = 10000n;
2
+ const address = (value, label) => {
3
+ if (typeof value !== 'string' || value.length === 0)
4
+ throw new Error(`${label} address required`);
5
+ return value;
6
+ };
7
+ const amount = (value, label, allowZero = false) => {
8
+ const result = BigInt(value);
9
+ if (allowZero ? result < 0n : result <= 0n)
10
+ throw new Error(`${label} must be ${allowZero ? 'non-negative' : 'positive'}`);
11
+ return result;
12
+ };
13
+ const safeInteger = (value, label) => {
14
+ if (!Number.isSafeInteger(value) || value < 0)
15
+ throw new Error(`${label} must be a non-negative safe integer`);
16
+ return value;
17
+ };
18
+ class DexClient {
19
+ transport;
20
+ factory;
21
+ constructor(transport, factory) {
22
+ if (!transport || typeof transport.staticCall !== 'function')
23
+ throw new Error('DEX transport with staticCall required');
24
+ this.transport = transport;
25
+ this.factory = address(factory, 'factory');
26
+ }
27
+ async poolFor(tokenA, tokenB) {
28
+ tokenA = address(tokenA, 'token A');
29
+ tokenB = address(tokenB, 'token B');
30
+ if (tokenA === tokenB)
31
+ throw new Error('pool tokens must differ');
32
+ const pool = await this.transport.staticCall(this.factory, 'poolFor', [tokenA, tokenB]);
33
+ return pool === undefined || pool === null || pool === '' ? undefined : String(pool);
34
+ }
35
+ async pools() {
36
+ const pools = await this.transport.staticCall(this.factory, 'pools', []);
37
+ if (!Array.isArray(pools))
38
+ throw new Error('factory returned an invalid pool list');
39
+ return pools.map(String);
40
+ }
41
+ async poolState(pool) {
42
+ pool = address(pool, 'pool');
43
+ const [token0, token1, feeBasisPoints, reserve0, reserve1, totalLiquidity] = await Promise.all([
44
+ this.transport.staticCall(pool, 'token0', []),
45
+ this.transport.staticCall(pool, 'token1', []),
46
+ this.transport.staticCall(pool, 'feeBasisPoints', []),
47
+ this.transport.staticCall(pool, 'reserve0', []),
48
+ this.transport.staticCall(pool, 'reserve1', []),
49
+ this.transport.staticCall(pool, 'totalLiquidity', [])
50
+ ]);
51
+ return {
52
+ address: pool,
53
+ token0: address(String(token0 ?? ''), 'token 0'),
54
+ token1: address(String(token1 ?? ''), 'token 1'),
55
+ feeBasisPoints: BigInt(feeBasisPoints),
56
+ reserve0: BigInt(reserve0),
57
+ reserve1: BigInt(reserve1),
58
+ totalLiquidity: BigInt(totalLiquidity)
59
+ };
60
+ }
61
+ async liquidityOf(pool, provider) {
62
+ pool = address(pool, 'pool');
63
+ provider = address(provider, 'provider');
64
+ return BigInt((await this.transport.staticCall(pool, 'liquidityOf', [provider])));
65
+ }
66
+ async quoteExactInput(pool, tokenIn, amountIn) {
67
+ pool = address(pool, 'pool');
68
+ tokenIn = address(tokenIn, 'input token');
69
+ const input = amount(amountIn, 'input amount');
70
+ return BigInt((await this.transport.staticCall(pool, 'getAmountOut', [tokenIn, input])));
71
+ }
72
+ minimumAmountOut(quote, slippageBasisPoints) {
73
+ const output = amount(quote, 'quote');
74
+ const slippage = amount(slippageBasisPoints, 'slippage', true);
75
+ if (slippage >= BASIS_POINTS)
76
+ throw new Error('slippage must be below 10000 basis points');
77
+ return (output * (BASIS_POINTS - slippage)) / BASIS_POINTS;
78
+ }
79
+ approve(from, token, pool, value, options = {}) {
80
+ return this.#transaction(from, token, 'approve', [address(pool, 'pool'), amount(value, 'approval amount')], options);
81
+ }
82
+ addLiquidity(from, pool, amount0Desired, amount1Desired, minimumLiquidity = 0n, options = {}) {
83
+ return this.#transaction(from, pool, 'addLiquidity', [
84
+ amount(amount0Desired, 'token 0 amount'),
85
+ amount(amount1Desired, 'token 1 amount'),
86
+ amount(minimumLiquidity, 'minimum liquidity', true)
87
+ ], options);
88
+ }
89
+ removeLiquidity(from, pool, liquidity, minimumAmount0 = 0n, minimumAmount1 = 0n, receiver = from, options = {}) {
90
+ return this.#transaction(from, pool, 'removeLiquidity', [
91
+ amount(liquidity, 'liquidity'),
92
+ amount(minimumAmount0, 'minimum token 0 amount', true),
93
+ amount(minimumAmount1, 'minimum token 1 amount', true),
94
+ address(receiver, 'receiver')
95
+ ], options);
96
+ }
97
+ swapExactInput(from, pool, tokenIn, amountIn, minimumAmountOut, receiver = from, deadline = Date.now() + 20 * 60 * 1000, options = {}) {
98
+ return this.#transaction(from, pool, 'swapExactTokensForTokens', [
99
+ address(tokenIn, 'input token'),
100
+ amount(amountIn, 'input amount'),
101
+ amount(minimumAmountOut, 'minimum output amount', true),
102
+ address(receiver, 'receiver'),
103
+ safeInteger(deadline, 'deadline')
104
+ ], options);
105
+ }
106
+ registerPool(from, pool, options = {}) {
107
+ return this.#transaction(from, this.factory, 'registerPool', [address(pool, 'pool')], options);
108
+ }
109
+ async signAndSend(transaction, signer) {
110
+ if (typeof signer !== 'function')
111
+ throw new Error('transaction signer required');
112
+ if (typeof this.transport.sendTransaction !== 'function')
113
+ throw new Error('DEX transport does not support sendTransaction');
114
+ const signed = await signer(transaction);
115
+ return this.transport.sendTransaction(signed);
116
+ }
117
+ async #transaction(from, to, method, params, options) {
118
+ from = address(from, 'sender');
119
+ to = address(to, 'target');
120
+ let nonce = options.nonce;
121
+ if (nonce === undefined) {
122
+ if (typeof this.transport.getNonce !== 'function')
123
+ throw new Error('nonce required when transport has no getNonce');
124
+ nonce = Number(await this.transport.getNonce(from)) + 1;
125
+ }
126
+ return {
127
+ from,
128
+ to,
129
+ method,
130
+ params,
131
+ nonce: safeInteger(nonce, 'nonce'),
132
+ timestamp: safeInteger(options.timestamp ?? Date.now(), 'timestamp')
133
+ };
134
+ }
135
+ }
136
+
137
+ export { DexClient as default };
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
@@ -1,5 +1,7 @@
1
1
  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
- import 'socket-request-client';
4
+ export { default as DexClient } from './dex.js';
5
+ import '@leofcoin/binary';
6
+ import './transport.js';
5
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
+ }
@@ -0,0 +1,45 @@
1
+ export type DexTransaction = {
2
+ from: string;
3
+ to: string;
4
+ method: string;
5
+ params: unknown[];
6
+ nonce: number;
7
+ timestamp: number;
8
+ };
9
+ export type DexTransport = {
10
+ staticCall: (contract: string, method: string, params?: unknown[]) => Promise<unknown>;
11
+ getNonce?: (address: string) => Promise<number | string | bigint>;
12
+ sendTransaction?: (transaction: unknown) => Promise<unknown>;
13
+ };
14
+ export type DexSigner = (transaction: DexTransaction) => Promise<unknown> | unknown;
15
+ export type TransactionOptions = {
16
+ nonce?: number;
17
+ timestamp?: number;
18
+ };
19
+ export type PoolState = {
20
+ address: string;
21
+ token0: string;
22
+ token1: string;
23
+ feeBasisPoints: bigint;
24
+ reserve0: bigint;
25
+ reserve1: bigint;
26
+ totalLiquidity: bigint;
27
+ };
28
+ export default class DexClient {
29
+ #private;
30
+ readonly transport: DexTransport;
31
+ readonly factory: string;
32
+ constructor(transport: DexTransport, factory: string);
33
+ poolFor(tokenA: string, tokenB: string): Promise<string | undefined>;
34
+ pools(): Promise<string[]>;
35
+ poolState(pool: string): Promise<PoolState>;
36
+ liquidityOf(pool: string, provider: string): Promise<bigint>;
37
+ quoteExactInput(pool: string, tokenIn: string, amountIn: bigint | number | string): Promise<bigint>;
38
+ minimumAmountOut(quote: bigint | number | string, slippageBasisPoints: bigint | number | string): bigint;
39
+ approve(from: string, token: string, pool: string, value: bigint | number | string, options?: TransactionOptions): Promise<DexTransaction>;
40
+ addLiquidity(from: string, pool: string, amount0Desired: bigint | number | string, amount1Desired: bigint | number | string, minimumLiquidity?: bigint | number | string, options?: TransactionOptions): Promise<DexTransaction>;
41
+ removeLiquidity(from: string, pool: string, liquidity: bigint | number | string, minimumAmount0?: bigint | number | string, minimumAmount1?: bigint | number | string, receiver?: string, options?: TransactionOptions): Promise<DexTransaction>;
42
+ swapExactInput(from: string, pool: string, tokenIn: string, amountIn: bigint | number | string, minimumAmountOut: bigint | number | string, receiver?: string, deadline?: number, options?: TransactionOptions): Promise<DexTransaction>;
43
+ registerPool(from: string, pool: string, options?: TransactionOptions): Promise<DexTransaction>;
44
+ signAndSend(transaction: DexTransaction, signer: DexSigner): Promise<unknown>;
45
+ }
@@ -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>;
@@ -1,3 +1,5 @@
1
1
  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
+ export { default as DexClient } from './dex.js';
5
+ export type { DexSigner, DexTransaction, DexTransport, PoolState, TransactionOptions } from './dex.js';
@@ -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.16",
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"
@@ -19,9 +20,14 @@
19
20
  "import": "./exports/direct.js",
20
21
  "types": "./exports/types/direct.d.ts"
21
22
  },
23
+ "./dex": {
24
+ "import": "./exports/dex.js",
25
+ "types": "./exports/types/dex.d.ts"
26
+ },
22
27
  "./ws.js": "./exports/ws.js",
23
28
  "./http.js": "./exports/http.js",
24
- "./direct.js": "./exports/direct.js"
29
+ "./direct.js": "./exports/direct.js",
30
+ "./dex.js": "./exports/dex.js"
25
31
  },
26
32
  "files": [
27
33
  "exports"
@@ -51,6 +57,6 @@
51
57
  },
52
58
  "dependencies": {
53
59
  "@leofcoin/utils": "^1.1.45",
54
- "socket-request-client": "^2.0.9"
60
+ "@leofcoin/binary": "^1.1.0"
55
61
  }
56
62
  }