@leofcoin/endpoints 0.4.11 → 0.5.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/README.md CHANGED
@@ -1,3 +1,10 @@
1
+ # Binary RPC migration
2
+
3
+ Protocol 0.5.0 uses `application/octet-stream` HTTP and binary-only WebSocket
4
+ frames. JSON bodies/query parameters and text socket frames are unsupported.
5
+ Use the matching `@leofcoin/endpoint-clients@0.4` rather than `response.json()`.
6
+ See [wire format, rollout and upstream release gates](../../docs/binary-wire-protocol.md).
7
+
1
8
  # endpoints
2
9
 
3
10
  ```js
@@ -6,4 +13,87 @@ http(chain, port, networkVersion)
6
13
 
7
14
  ```js
8
15
  wss(chain, port, networkVersion)
9
- ```
16
+ ```
17
+
18
+ ## Generic contract reads
19
+
20
+ POST /staticCall accepts a binary map containing `contract`, `method` and an
21
+ array `params` (default `[]`). Native BigInts and byte arrays retain their types;
22
+ there is no JSON boxing or revival. Use the matching HTTP/WebSocket clients.
23
+ Malformed parameters return HTTP 400; rejected read-only queries return HTTP 422.
24
+ All reads delegate to the chain's isolated `staticCall` path.
25
+
26
+ ## Public read-only explorer
27
+
28
+ Deploy/restart the updated endpoints build before enabling anonymous browser
29
+ exploration. These binary POST routes never access wallets, select accounts, sign,
30
+ submit transactions or fetch missing content from peers. Responses carry
31
+ `apiVersion: 1` and `chainId`. Except status, queries must include the expected
32
+ `chainId`; a changed chain returns HTTP 409. Non-POST requests return HTTP 405.
33
+
34
+ | Route | Binary fields | Result |
35
+ | --- | --- | --- |
36
+ | `/explorer/status` | none | Genesis identity, source tip, connected peer IDs, sync state |
37
+ | `/explorer/blocks` | `from`, `count` (1–25), `chainId` | Applied block snapshot, zero-based indexes |
38
+ | `/explorer/block` | `hash`, `chainId` | Hash-checked local content as Uint8Array, maximum 1 MiB |
39
+ | `/explorer/transaction` | `hash`, `chainId` | Hash-checked local transaction/pool content as Uint8Array |
40
+ | `/explorer/pool` | `chainId` | Up to 101 pool hashes; clients display at most 100 |
41
+ | `/explorer/overview` | `chainId` | Public counters, registry, top-five holder balances and shares |
42
+
43
+ Reads time out after 10 seconds. A local content miss returns HTTP 404 rather
44
+ than a proxy fetch. Source sync/membership is not a proof of network finality.
45
+ No explorer write action is exposed; all application API responses use binary encoding.
46
+
47
+ ## Chain Manager HTTP API
48
+
49
+ `POST /manager/rpc` accepts `application/octet-stream` (maximum 1 MiB). Requests must
50
+ include the full expected `chainId`; the node verifies its own genesis identity
51
+ and returns HTTP 409 on a mismatch. All application API responses use binary encoding.
52
+ This API never unlocks a node wallet, selects remote accounts or signs messages.
53
+ Deploy/restart an updated endpoints package before connecting Chain Manager.
54
+
55
+ Actions:
56
+
57
+ | Action | Additional fields | Result |
58
+ | --- | --- | --- |
59
+ | `status` | optional public `account` | API version, chain identity, sync, peer count, height, contract hashes, account balance in integer units and latest nonce |
60
+ | `read` | `contract`, `method`, array `params` | `{ value }`, with native BigInt/byte fields |
61
+ | `submit` | signed `transaction`, optional encoded `contract` | transaction hash and explicit status |
62
+ | `receipt` | transaction `hash` | durable status, canonical inclusion (`blockHeight`, `blockHash`) and scan progress (`scannedHeight`) when observed, or `unknown` |
63
+
64
+ Submissions require a synced node with connected peers. A contract upload must
65
+ match the signed factory registration, creator and content hash. The node
66
+ validates signatures before storing uploaded code. Duplicate transaction hashes
67
+ are admitted once, including concurrent duplicates.
68
+
69
+ `pending` means admitted, not confirmed. The chain's canonical completion
70
+ promise or verified canonical block membership produces `confirmed`; execution
71
+ rejection produces `failed`. Unverifiable admission
72
+ produces `unknown`, never a success-shaped response or an automatic resend.
73
+
74
+ Before any contract upload/admission, a reservation is durably saved in the
75
+ node's existing isolated `chainStore`, under `manager:receipts:v1:<full-chain-id>`.
76
+ Storage unavailability/corruption/write failure blocks admission explicitly.
77
+ Updates and duplicate checks are serialized. Restarting the HTTP server/node
78
+ restores admission history and does not resubmit a reserved transaction.
79
+
80
+ After restart, `receipt` scans at most 128 canonical blocks per request from the
81
+ submission height, persists its cursor and checks recorded inclusion/scan anchors.
82
+ It uses canonical worker blocks, not the mempool or raw transaction store; absent
83
+ inclusion is `unknown`, never inferred failure. Refresh again to continue a long
84
+ scan. A changing tip or unavailable canonical block returns an explicit error
85
+ without advancing durable progress. Canonical tip lookup avoids cached fallback
86
+ state when the machine exposes its strict getter.
87
+
88
+ Receipts are bounded to 512 entries per chain. Only completed entries may be
89
+ evicted; a full unresolved journal blocks new admission. Reservations written
90
+ before a crash can remain unknown if delivery never happened. Evicted entries,
91
+ pre-upgrade transactions and receipts from another node are also unknown; these
92
+ require operator reconciliation and are not proof of rejection. The journal is
93
+ local to a node, not shared across distinct node instances.
94
+
95
+ Use HTTPS and an operator-controlled endpoint. CORS permits the web and Electron
96
+ clients to connect but is not authentication. The API only accepts transactions
97
+ already signed by the client; it provides no privileged server-side keys.
98
+ Chain-ID checks protect this connection from accidental chain switching; they do
99
+ not add cryptographic cross-chain replay protection to the existing wire format.
@@ -0,0 +1,4 @@
1
+ export declare const RPC_LIMIT: number;
2
+ export declare function readBinaryRequest(request: any, maxBytes?: number): Promise<any>;
3
+ /** The Koa boundary owns serialization, including errors. Domain handlers see native values. */
4
+ export declare const binaryHttp: (ctx: any, next: any) => Promise<any>;
@@ -0,0 +1,54 @@
1
+ import { encodeWire, decodeWire, BINARY_CONTENT_TYPE } from '@leofcoin/binary';
2
+ export const RPC_LIMIT = 4 * 1024 * 1024;
3
+ export async function readBinaryRequest(request, maxBytes = 1048576) {
4
+ const chunks = [];
5
+ let size = 0;
6
+ for await (const chunk of request) {
7
+ size += chunk.length;
8
+ if (size > maxBytes)
9
+ throw Object.assign(new Error('Binary request too large'), { status: 413 });
10
+ chunks.push(chunk);
11
+ }
12
+ return decodeWire('rpc', Buffer.concat(chunks), { maxBytes });
13
+ }
14
+ /** The Koa boundary owns serialization, including errors. Domain handlers see native values. */
15
+ export const binaryHttp = async (ctx, next) => {
16
+ if (ctx.path === '/' && ctx.method === 'GET')
17
+ return next();
18
+ try {
19
+ if (ctx.method === 'POST') {
20
+ if (ctx.get('content-type').split(';')[0] !== BINARY_CONTENT_TYPE)
21
+ throw Object.assign(new Error('Use application/octet-stream binary RPC'), { status: 415 });
22
+ try {
23
+ ctx.state.rpcParams = await readBinaryRequest(ctx.req);
24
+ }
25
+ catch (error) {
26
+ throw Object.assign(error, { status: error.status || 400 });
27
+ }
28
+ if (!ctx.state.rpcParams || typeof ctx.state.rpcParams !== 'object' || Array.isArray(ctx.state.rpcParams) || ctx.state.rpcParams instanceof Uint8Array)
29
+ throw Object.assign(new Error('RPC parameters must be a binary map'), { status: 400 });
30
+ }
31
+ else if (ctx.method === 'GET' && !ctx.querystring)
32
+ ctx.state.rpcParams = {};
33
+ else
34
+ throw Object.assign(new Error('Use binary POST parameters; JSON/query-string payloads are unsupported'), { status: 405 });
35
+ await next();
36
+ // Koa turns a legitimate null result into HTTP 204; binary RPC must still carry its null tag.
37
+ if (ctx.status === 204 && ctx.body == null)
38
+ ctx.status = 200;
39
+ else if (ctx.body == null)
40
+ ctx.body = { error: ctx.message || 'RPC returned no value' };
41
+ }
42
+ catch (error) {
43
+ ctx.status = Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 ? error.status : 500;
44
+ ctx.body = { error: ctx.status === 500 ? 'RPC operation failed' : error.message };
45
+ }
46
+ try {
47
+ ctx.body = Buffer.from(encodeWire('rpc', ctx.body, { maxBytes: RPC_LIMIT }));
48
+ }
49
+ catch {
50
+ ctx.status = 500;
51
+ ctx.body = Buffer.from(encodeWire('rpc', { error: 'RPC response cannot be encoded' }));
52
+ }
53
+ ctx.type = BINARY_CONTENT_TYPE;
54
+ };
@@ -0,0 +1,4 @@
1
+ export default function binarySocketServer({ port, protocol }: {
2
+ port: any;
3
+ protocol: any;
4
+ }, routes: any): any;
@@ -0,0 +1,57 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import { encodeWire, decodeWire } from '@leofcoin/binary';
3
+ export default function binarySocketServer({ port, protocol }, routes) {
4
+ const required = `${protocol}.lfc-binary-v1`;
5
+ const server = new WebSocketServer({ port, maxPayload: 4 * 1024 * 1024,
6
+ handleProtocols: protocols => protocols.has(required) ? required : false });
7
+ server.on('connection', socket => {
8
+ if (socket.protocol !== required) {
9
+ socket.close(1002, 'Binary protocol required');
10
+ return;
11
+ }
12
+ let active = 0;
13
+ socket.on('error', () => { });
14
+ socket.on('message', async (bytes, binary) => {
15
+ if (!binary || !(bytes instanceof Uint8Array)) {
16
+ socket.close(1003, 'Binary frames required');
17
+ return;
18
+ }
19
+ let request;
20
+ try {
21
+ request = decodeWire('rpc-request', bytes, { maxBytes: 1048576 });
22
+ if (!request || typeof request.id !== 'string' || request.id.length > 80 || !request.id ||
23
+ typeof request.url !== 'string' || !Object.hasOwn(routes, request.url))
24
+ throw new Error('Invalid RPC request');
25
+ }
26
+ catch {
27
+ socket.close(1002, 'Invalid binary RPC');
28
+ return;
29
+ }
30
+ const send = (value, status = 200) => {
31
+ if (socket.readyState === 1)
32
+ socket.send(encodeWire('rpc-response', { id: request.id, status, value }, { maxBytes: 4 * 1024 * 1024 }));
33
+ };
34
+ if (active >= 32) {
35
+ send({ error: 'Too many pending requests' }, 429);
36
+ return;
37
+ }
38
+ active++;
39
+ try {
40
+ const context = { send };
41
+ await routes[request.url](...(request.params === undefined ? [context] : [request.params, context]));
42
+ }
43
+ catch {
44
+ try {
45
+ send({ error: 'RPC operation failed' }, 500);
46
+ }
47
+ catch {
48
+ socket.close(1011);
49
+ }
50
+ }
51
+ finally {
52
+ active--;
53
+ }
54
+ });
55
+ });
56
+ return server;
57
+ }
@@ -0,0 +1,2 @@
1
+ /** Public data only. No wallet, account selection, signing, submission or peer fetches. */
2
+ export declare function explorerMiddleware(chain: any): (ctx: any, next: any) => Promise<any>;
@@ -0,0 +1,117 @@
1
+ import { BlockMessage, TransactionMessage } from '@leofcoin/messages';
2
+ import { verifyChainIdentity } from '@leofcoin/lib/chain-identity';
3
+ const fail = (status, message) => { throw Object.assign(new Error(message), { status }); };
4
+ const integer = (value, maximum) => {
5
+ if (!/^(0|[1-9]\d*)$/.test(String(value)) || !Number.isSafeInteger(Number(value)) || Number(value) > maximum)
6
+ fail(400, 'Invalid explorer range');
7
+ return Number(value);
8
+ };
9
+ const hashInput = value => {
10
+ if (typeof value !== 'string' || !/^[A-Z2-7]{32,128}$/.test(value))
11
+ fail(400, 'Invalid content hash');
12
+ return value;
13
+ };
14
+ /** Public data only. No wallet, account selection, signing, submission or peer fetches. */
15
+ export function explorerMiddleware(chain) {
16
+ return async (ctx, next) => {
17
+ if (!ctx.path.startsWith('/explorer/'))
18
+ return next();
19
+ ctx.set('Cache-Control', 'no-store');
20
+ if (ctx.method !== 'POST') {
21
+ ctx.status = 405;
22
+ ctx.body = { error: 'Explorer supports read-only binary POST requests' };
23
+ return;
24
+ }
25
+ let timer;
26
+ try {
27
+ const body = await Promise.race([
28
+ (async () => {
29
+ const identity = await verifyChainIdentity(await chain.getChainIdentity());
30
+ if (ctx.path !== '/explorer/status' && ctx.state.rpcParams.chainId !== identity.chainId)
31
+ fail(409, 'Explorer chain identity changed; reconnect');
32
+ const response = { apiVersion: 1, chainId: identity.chainId };
33
+ if (ctx.path === '/explorer/status') {
34
+ const tip = await chain.lastBlock;
35
+ return { ...response, identity, tip: { index: tip.index, hash: tip.hash },
36
+ synced: chain.state?.sync === 'synced', nativeToken: identity.genesisContracts.nativeToken,
37
+ peers: Object.entries(globalThis.peernet?.connections || {})
38
+ .filter(([, peer]) => peer?.connected).map(([id]) => id).slice(0, 100) };
39
+ }
40
+ if (ctx.path === '/explorer/blocks') {
41
+ const from = integer(ctx.state.rpcParams.from, Number.MAX_SAFE_INTEGER);
42
+ const count = integer(ctx.state.rpcParams.count, 25);
43
+ if (count < 1 || from > Number.MAX_SAFE_INTEGER - count)
44
+ fail(400, 'Invalid explorer range');
45
+ const blocks = await chain.getBlocks(from, from + count);
46
+ if (!Array.isArray(blocks))
47
+ fail(503, 'Canonical block data unavailable');
48
+ return { ...response, blocks: blocks.filter(block => block && block.loaded !== false) };
49
+ }
50
+ if (ctx.path === '/explorer/block' || ctx.path === '/explorer/transaction') {
51
+ const hash = hashInput(ctx.state.rpcParams.hash);
52
+ const stores = ctx.path === '/explorer/block' ? [globalThis.blockStore]
53
+ : [globalThis.transactionStore, globalThis.transactionPoolStore];
54
+ let bytes;
55
+ for (const store of stores) {
56
+ if (store && await store.has(hash)) {
57
+ bytes = await store.get(hash);
58
+ break;
59
+ }
60
+ }
61
+ if (!(bytes instanceof Uint8Array) || bytes.length === 0)
62
+ fail(404, 'Content not available on this node');
63
+ if (bytes.length > 1_048_576)
64
+ fail(413, 'Explorer content exceeds 1 MiB');
65
+ const message = ctx.path === '/explorer/block' ? new BlockMessage(bytes) : new TransactionMessage(bytes);
66
+ if (await message.hash() !== hash)
67
+ fail(503, 'Stored content hash mismatch');
68
+ return { ...response, encoded: bytes };
69
+ }
70
+ if (ctx.path === '/explorer/pool') {
71
+ if (!globalThis.transactionPoolStore)
72
+ fail(503, 'Transaction pool unavailable');
73
+ return { ...response, hashes: (await globalThis.transactionPoolStore.keys(101)).slice(0, 101).map(String) };
74
+ }
75
+ if (ctx.path === '/explorer/overview') {
76
+ const [validators, registeredContracts, nativeCalls, nativeMints, nativeBurns, nativeTransfers, totalContracts, totalTransactions, totalSize, poolCount, poolSize, balances] = await Promise.all([
77
+ chain.staticCall(identity.genesisContracts.validators, 'validators', []),
78
+ chain.staticCall(identity.genesisContracts.contractFactory, 'totalContracts', []),
79
+ chain.nativeCalls, chain.nativeMints, chain.nativeBurns, chain.nativeTransfers,
80
+ chain.totalContracts, chain.totalTransactions, chain.totalSize,
81
+ globalThis.transactionPoolStore?.length(), globalThis.transactionPoolStore?.size(), chain.balances
82
+ ]);
83
+ let supply = 0n;
84
+ let funded = 0;
85
+ const top = [];
86
+ if (!balances || typeof balances !== 'object')
87
+ fail(503, 'Balances unavailable');
88
+ for (const [address, raw] of Object.entries(balances)) {
89
+ const balance = BigInt(raw);
90
+ if (balance <= 0n)
91
+ continue;
92
+ supply += balance;
93
+ funded++;
94
+ top.push({ address, balance });
95
+ top.sort((a, b) => a.balance === b.balance ? a.address.localeCompare(b.address) : a.balance > b.balance ? -1 : 1);
96
+ if (top.length > 5)
97
+ top.pop();
98
+ }
99
+ return { ...response, validators, registeredContracts, nativeCalls, nativeMints, nativeBurns,
100
+ nativeTransfers, totalContracts, totalTransactions, totalSize, poolCount, poolSize,
101
+ holders: { total: funded, holders: top.map(holder => ({ ...holder, share: Number(holder.balance * 10000n / (supply || 1n)) / 100 })) } };
102
+ }
103
+ fail(404, 'Unknown read-only explorer resource');
104
+ })(),
105
+ new Promise((_, reject) => { timer = setTimeout(() => reject(Object.assign(new Error('Explorer read timed out'), { status: 503 })), 10000); })
106
+ ]);
107
+ ctx.body = body;
108
+ }
109
+ catch (error) {
110
+ ctx.status = [400, 404, 405, 409, 413, 503].includes(error.status) ? error.status : 503;
111
+ ctx.body = { error: ctx.status === 503 ? 'Public explorer data unavailable; retry after node sync' : error.message };
112
+ }
113
+ finally {
114
+ clearTimeout(timer);
115
+ }
116
+ };
117
+ }
package/exports/http.js CHANGED
@@ -1,12 +1,15 @@
1
+ import { binaryHttp } from './binary-http.js';
1
2
  import koa from 'koa';
2
3
  import cors from '@koa/cors';
3
4
  import Router from '@koa/router';
4
- import { formatUnits, jsonStringifyBigInt } from '@leofcoin/utils';
5
+ import { formatUnits } from '@leofcoin/utils';
5
6
  import { readFile } from 'fs/promises';
6
7
  import Showdown from 'showdown';
7
8
  import shared from './shared.js';
8
9
  import { dirname, join } from 'path';
9
10
  import { fileURLToPath } from 'url';
11
+ import { managerMiddleware } from './manager.js';
12
+ import { explorerMiddleware } from './explorer.js';
10
13
  const __filename = fileURLToPath(import.meta.url);
11
14
  const __dirname = dirname(__filename);
12
15
  const converter = new Showdown.Converter();
@@ -22,39 +25,39 @@ export default (chain, port, networkVersion, remote = false) => {
22
25
  router.get('/', (ctx) => {
23
26
  ctx.body = converter.makeHtml(apiFile.toString());
24
27
  });
25
- router.get('/network', (ctx) => {
28
+ router.all('/network', (ctx) => {
26
29
  ctx.body = {
27
30
  version: networkVersion
28
31
  };
29
32
  });
30
- router.get('/chainIdentity', async (ctx) => {
33
+ router.all('/chainIdentity', async (ctx) => {
31
34
  ctx.body = await chain.getChainIdentity();
32
35
  });
33
- router.get('/networkStats', async (ctx) => {
36
+ router.all('/networkStats', async (ctx) => {
34
37
  ctx.body = await shared.networkStats(chain, networkVersion);
35
38
  });
36
- router.get('/balances', async (ctx) => {
39
+ router.all('/balances', async (ctx) => {
37
40
  ctx.body = await chain.balances;
38
41
  });
39
- router.get('/balanceOf', async (ctx) => {
42
+ router.all('/balanceOf', async (ctx) => {
40
43
  // @ts-ignore
41
- const balance = (await chain.balances)[ctx.query.address];
44
+ const balance = (await chain.balances)[ctx.state.rpcParams.address];
42
45
  if (balance)
43
- ctx.body = ctx.query.format ? formatUnits(balance).toString() : balance.toString();
46
+ ctx.body = ctx.state.rpcParams.format ? formatUnits(balance).toString() : balance.toString();
44
47
  else
45
48
  ctx.body = '0n';
46
49
  });
47
- router.get('/selectedAccount', (ctx) => {
50
+ router.all('/selectedAccount', (ctx) => {
48
51
  if (remote)
49
52
  ctx.body = globalThis.peernet.selectedAccount;
50
53
  else
51
54
  ctx.status = 405;
52
55
  });
53
- router.get('/selectAccount', async (ctx) => {
56
+ router.all('/selectAccount', async (ctx) => {
54
57
  if (remote) {
55
- globalThis.peernet.selectedAccount = ctx.query.address;
58
+ globalThis.peernet.selectedAccount = ctx.state.rpcParams.address;
56
59
  try {
57
- await globalThis.walletStore.put('selected-account', ctx.query.address);
60
+ await globalThis.walletStore.put('selected-account', ctx.state.rpcParams.address);
58
61
  }
59
62
  catch (error) {
60
63
  ctx.body = error.message;
@@ -64,97 +67,115 @@ export default (chain, port, networkVersion, remote = false) => {
64
67
  ctx.status = 405;
65
68
  }
66
69
  });
67
- router.get('/accounts', async (ctx) => {
70
+ router.all('/accounts', async (ctx) => {
68
71
  if (remote)
69
72
  ctx.body = JSON.parse(new TextDecoder().decode(await globalThis.walletStore.get('accounts')));
70
73
  else
71
74
  ctx.status = 405;
72
75
  });
73
- router.get('/hasTransactionToHandle', async (ctx) => {
76
+ router.all('/hasTransactionToHandle', async (ctx) => {
74
77
  ctx.body = await chain.hasTransactionToHandle();
75
78
  });
76
- router.get('/getNonce', async (ctx) => {
77
- ctx.body = await chain.getNonce(ctx.query.address);
79
+ router.all('/getNonce', async (ctx) => {
80
+ ctx.body = await chain.getNonce(ctx.state.rpcParams.address);
78
81
  });
79
- router.get('/getBlock', async (ctx) => (ctx.body = await chain.getBlock(ctx.query.index)));
80
- router.get('/blocks', async (ctx) => {
81
- const limit = ctx.query.amount;
82
+ router.all('/getBlock', async (ctx) => (ctx.body = await chain.getBlock(ctx.state.rpcParams.index)));
83
+ router.all('/blocks', async (ctx) => {
84
+ const limit = ctx.state.rpcParams.amount;
82
85
  ctx.body = await globalThis.blockStore.values(limit);
83
86
  });
84
- router.get('/sendTransaction', async (ctx) => {
87
+ router.all('/sendTransaction', async (ctx) => {
85
88
  try {
86
- const tx = await chain.sendTransaction(ctx.query.transaction);
87
- await tx.wait();
89
+ const tx = await chain.sendTransaction((ctx.state.rpcParams.transaction ?? ctx.state.rpcParams));
90
+ await (typeof tx.wait === 'function' ? tx.wait() : tx.wait);
88
91
  delete tx.wait;
89
- ctx.body = JSON.stringify(tx, jsonStringifyBigInt);
92
+ ctx.body = tx;
90
93
  }
91
94
  catch (error) {
92
- // ctx(202)
95
+ ctx.status = 422;
96
+ ctx.body = { error: error instanceof Error ? error.message : 'Transaction failed' };
93
97
  }
94
98
  });
95
- router.get('/peerId', (ctx) => (ctx.body = globalThis.peernet.peerId));
96
- router.get('/validators', (ctx) => (ctx.body = chain.validators));
97
- router.get('/peers', (ctx) => (ctx.body = globalThis.peernet.peers.map(([id, peer]) => id)));
98
- router.get('/lookup', async (ctx) => {
99
- ctx.body = await chain.lookup(ctx.query.name);
100
- });
101
- router.get('/staticCall', async (ctx) => (ctx.body = await chain.staticCall(ctx.query.contract, ctx.query.method, ctx.query.params)));
102
- router.get('/contracts', async (ctx) => (ctx.body = await chain.contracts));
103
- router.get('/totalContracts', async (ctx) => (ctx.body = await chain.totalContracts));
104
- router.get('/nativeToken', async (ctx) => (ctx.body = await chain.nativeToken));
105
- router.get('/nativeCalls', async (ctx) => (ctx.body = await chain.nativeCalls));
106
- router.get('/nativeMints', async (ctx) => (ctx.body = await chain.nativeMints));
107
- router.get('/nativeBurns', async (ctx) => (ctx.body = await chain.nativeBurns));
108
- router.get('/nativeTransfers', async (ctx) => (ctx.body = await chain.nativeTransfers));
109
- router.get('/totalBurnAmount', async (ctx) => (ctx.body = await chain.totalBurnAmount));
110
- router.get('/totalMintAmount', async (ctx) => (ctx.body = await chain.totalMintAmount));
111
- router.get('/totalTransferAmount', async (ctx) => (ctx.body = await chain.totalTransferAmount));
112
- router.get('/totalTransactions', async (ctx) => (ctx.body = await chain.totalTransactions));
113
- router.get('/totalSize', async (ctx) => (ctx.body = await chain.totalSize));
114
- router.get('/totalBlocks', async (ctx) => (ctx.body = await chain.totalBlocks));
115
- router.get('/lastBlock', async (ctx) => (ctx.body = await chain.lastBlock));
116
- router.get('/lastBlockHeight', async (ctx) => (ctx.body = await chain.lastBlockHeight));
117
- router.get('/poolTransactions', async (ctx) => (ctx.body = await globalThis.transactionPoolStore.get()));
118
- router.get('/transactionsInPool', async (ctx) => (ctx.body = await globalThis.transactionPoolStore.length()));
119
- router.get('/transactionPoolSize', async (ctx) => (ctx.body = await globalThis.transactionPoolStore.size()));
120
- router.get('/participating', (ctx) => (ctx.body = chain.participating));
121
- router.get('/participate', async (ctx) => {
99
+ router.all('/peerId', (ctx) => (ctx.body = globalThis.peernet.peerId));
100
+ router.all('/validators', (ctx) => (ctx.body = chain.validators));
101
+ router.all('/peers', (ctx) => (ctx.body = globalThis.peernet.peers.map(([id, peer]) => id)));
102
+ router.all('/lookup', async (ctx) => {
103
+ ctx.body = await chain.lookup(ctx.state.rpcParams.name);
104
+ });
105
+ router.all('/staticCall', async (ctx) => {
106
+ let params;
107
+ try {
108
+ params = ctx.state.rpcParams.params ?? [];
109
+ if (!Array.isArray(params) || typeof ctx.state.rpcParams.contract !== 'string' || typeof ctx.state.rpcParams.method !== 'string')
110
+ throw new Error('Invalid read parameters');
111
+ }
112
+ catch {
113
+ ctx.status = 400;
114
+ ctx.body = { error: 'staticCall params must be a binary array' };
115
+ return;
116
+ }
117
+ try {
118
+ const value = await chain.staticCall(ctx.state.rpcParams.contract, ctx.state.rpcParams.method, params);
119
+ ctx.body = value ?? null;
120
+ }
121
+ catch (error) {
122
+ ctx.status = 422;
123
+ ctx.body = { error: error instanceof Error ? error.message : 'Read-only contract query failed' };
124
+ }
125
+ });
126
+ router.all('/contracts', async (ctx) => (ctx.body = await chain.contracts));
127
+ router.all('/totalContracts', async (ctx) => (ctx.body = await chain.totalContracts));
128
+ router.all('/nativeToken', async (ctx) => (ctx.body = await chain.nativeToken));
129
+ router.all('/nativeCalls', async (ctx) => (ctx.body = await chain.nativeCalls));
130
+ router.all('/nativeMints', async (ctx) => (ctx.body = await chain.nativeMints));
131
+ router.all('/nativeBurns', async (ctx) => (ctx.body = await chain.nativeBurns));
132
+ router.all('/nativeTransfers', async (ctx) => (ctx.body = await chain.nativeTransfers));
133
+ router.all('/totalBurnAmount', async (ctx) => (ctx.body = await chain.totalBurnAmount));
134
+ router.all('/totalMintAmount', async (ctx) => (ctx.body = await chain.totalMintAmount));
135
+ router.all('/totalTransferAmount', async (ctx) => (ctx.body = await chain.totalTransferAmount));
136
+ router.all('/totalTransactions', async (ctx) => (ctx.body = await chain.totalTransactions));
137
+ router.all('/totalSize', async (ctx) => (ctx.body = await chain.totalSize));
138
+ router.all('/totalBlocks', async (ctx) => (ctx.body = await chain.totalBlocks));
139
+ router.all('/lastBlock', async (ctx) => (ctx.body = await chain.lastBlock));
140
+ router.all('/lastBlockHeight', async (ctx) => (ctx.body = await chain.lastBlockHeight));
141
+ router.all('/poolTransactions', async (ctx) => (ctx.body = await globalThis.transactionPoolStore.get()));
142
+ router.all('/transactionsInPool', async (ctx) => (ctx.body = await globalThis.transactionPoolStore.length()));
143
+ router.all('/transactionPoolSize', async (ctx) => (ctx.body = await globalThis.transactionPoolStore.size()));
144
+ router.all('/participating', (ctx) => (ctx.body = chain.participating));
145
+ router.all('/participate', async (ctx) => {
122
146
  if (remote)
123
- ctx.body = await chain.participate(ctx.query.address);
147
+ ctx.body = await chain.participate(ctx.state.rpcParams.address);
124
148
  else
125
149
  ctx.status = 405;
126
150
  });
127
- router.get('/deployContract', async (ctx) => {
151
+ router.all('/deployContract', async (ctx) => {
128
152
  try {
129
- const tx = await chain.deployContract(ctx.query.contract, ctx.query.transaction);
130
- await tx.wait();
153
+ const tx = await chain.deployContract(ctx.state.rpcParams.contract, ctx.state.rpcParams.transaction);
154
+ await (typeof tx.wait === 'function' ? tx.wait() : tx.wait);
131
155
  delete tx.wait;
132
- ctx.body = JSON.stringify(tx, jsonStringifyBigInt);
156
+ ctx.body = tx;
133
157
  }
134
158
  catch (error) {
135
- ctx.body = error.mesage;
159
+ ctx.status = 422;
160
+ ctx.body = { error: error instanceof Error ? error.message : 'Deployment failed' };
136
161
  }
137
162
  });
138
- router.get('/createContractAddress', async (ctx) => {
163
+ router.all('/createContractAddress', async (ctx) => {
139
164
  try {
140
- const address = await chain.createContractAddress(ctx.query.owner, ctx.query.code, ctx.query.params);
165
+ const address = await chain.createContractAddress(ctx.state.rpcParams.owner, ctx.state.rpcParams.code, ctx.state.rpcParams.params);
141
166
  ctx.body = address;
142
167
  }
143
168
  catch (error) { }
144
169
  });
145
- router.get('/blockHashMap', async (ctx) => (ctx.body = chain.blockHashMap));
146
- router.get('/bootstrap', async (ctx) => {
147
- ctx.body = (await shared.bootstrap()).map((value) => Array.from(value.toString()));
170
+ router.all('/blockHashMap', async (ctx) => (ctx.body = chain.blockHashMap));
171
+ router.all('/bootstrap', async (ctx) => {
172
+ ctx.body = await shared.bootstrap();
148
173
  });
149
174
  // todo finish ...
150
175
  api.use(cors({ origin: '*' }));
151
- api.use(async (ctx, next) => {
152
- await next();
153
- if (ctx.body !== null && typeof ctx.body === 'object') {
154
- ctx.body = JSON.stringify(ctx.body, jsonStringifyBigInt);
155
- ctx.type = 'application/json';
156
- }
157
- });
176
+ api.use(binaryHttp);
177
+ api.use(explorerMiddleware(chain));
178
+ api.use(managerMiddleware(chain));
158
179
  api.use(router.routes());
159
180
  return api.listen(port);
160
181
  };
@@ -0,0 +1,43 @@
1
+ type Store = {
2
+ has(key: string): Promise<boolean>;
3
+ get(key: string): Promise<Uint8Array | string>;
4
+ put(key: string, value: Uint8Array): Promise<unknown>;
5
+ };
6
+ type Block = {
7
+ index: number | bigint | string;
8
+ hash: string;
9
+ transactions: string[];
10
+ };
11
+ type Chain = {
12
+ lastBlock: Block | Promise<Block>;
13
+ machine?: {
14
+ getCanonicalTip(): Promise<Block>;
15
+ };
16
+ getBlocks(from: number, to: number): Promise<Block[]>;
17
+ };
18
+ export type Receipt = {
19
+ hash: string;
20
+ status: string;
21
+ createdAt: number;
22
+ error?: string;
23
+ startHeight: number;
24
+ scannedHeight?: number;
25
+ scannedHash?: string;
26
+ blockHeight?: number;
27
+ blockHash?: string;
28
+ };
29
+ /** Uses the node's already isolated durable chain store, never a shared filesystem path. */
30
+ export declare class ManagerReceipts {
31
+ #private;
32
+ private store;
33
+ private chain;
34
+ constructor(store: () => Store, chain: Chain);
35
+ tip(): Promise<Block>;
36
+ serialized<T>(work: () => Promise<T>): Promise<T>;
37
+ load(chainId: string): Promise<Map<string, Receipt>>;
38
+ save(chainId: string, receipts: Map<string, Receipt>): Promise<void>;
39
+ reserve(receipts: Map<string, Receipt>, receipt: Receipt): void;
40
+ complete(chainId: string, hash: string, status: 'confirmed' | 'failed', error?: string): Promise<void>;
41
+ reconcile(receipt: Receipt): Promise<Receipt>;
42
+ }
43
+ export {};
@@ -0,0 +1,117 @@
1
+ const statuses = ['submitting', 'pending', 'confirmed', 'failed', 'unknown'];
2
+ const LIMIT = 512;
3
+ /** Uses the node's already isolated durable chain store, never a shared filesystem path. */
4
+ export class ManagerReceipts {
5
+ store;
6
+ chain;
7
+ #tail = Promise.resolve();
8
+ constructor(store, chain) {
9
+ this.store = store;
10
+ this.chain = chain;
11
+ }
12
+ async tip() {
13
+ return this.chain.machine ? this.chain.machine.getCanonicalTip() : await this.chain.lastBlock;
14
+ }
15
+ async #block(index) {
16
+ const blocks = await this.chain.getBlocks(index, index + 1);
17
+ const block = blocks[0];
18
+ if (!block || Number(block.index) !== index || !Array.isArray(block.transactions) || typeof block.hash !== 'string')
19
+ throw new Error(`Canonical block ${index} is unavailable; retry reconciliation later`);
20
+ return block;
21
+ }
22
+ serialized(work) {
23
+ const next = this.#tail.then(work);
24
+ this.#tail = next.catch(error => { console.error('Management receipt operation failed:', error); });
25
+ return next;
26
+ }
27
+ async load(chainId) {
28
+ const store = this.store();
29
+ if (!store)
30
+ throw new Error('Durable chain store is unavailable; management writes are disabled');
31
+ const key = `manager:receipts:v1:${chainId}`;
32
+ if (!await store.has(key))
33
+ return new Map();
34
+ const raw = await store.get(key);
35
+ const parsed = JSON.parse(typeof raw === 'string' ? raw : new TextDecoder().decode(raw));
36
+ if (!Array.isArray(parsed) || parsed.length > LIMIT || parsed.some(value => !value || typeof value.hash !== 'string' || !/^[A-Z2-7]{32,128}$/.test(value.hash) ||
37
+ !statuses.includes(value.status) || !Number.isSafeInteger(value.createdAt) ||
38
+ !Number.isSafeInteger(value.startHeight) || value.startHeight < 0 ||
39
+ (value.scannedHeight !== undefined && (!Number.isSafeInteger(value.scannedHeight) || value.scannedHeight < 0)) ||
40
+ (value.blockHeight !== undefined && (!Number.isSafeInteger(value.blockHeight) || value.blockHeight < 1)) ||
41
+ (value.blockHash !== undefined && typeof value.blockHash !== 'string') ||
42
+ (value.scannedHash !== undefined && typeof value.scannedHash !== 'string') ||
43
+ (value.error !== undefined && typeof value.error !== 'string')))
44
+ throw new Error('Stored management receipts are invalid; refusing to overwrite them');
45
+ return new Map(parsed.map(receipt => [receipt.hash, receipt]));
46
+ }
47
+ async save(chainId, receipts) {
48
+ await this.store().put(`manager:receipts:v1:${chainId}`, new TextEncoder().encode(JSON.stringify([...receipts.values()])));
49
+ }
50
+ reserve(receipts, receipt) {
51
+ if (receipts.size >= LIMIT) {
52
+ const completed = [...receipts].find(([, value]) => ['confirmed', 'failed'].includes(value.status));
53
+ if (!completed)
54
+ throw new Error('Receipt capacity reached; reconcile pending transactions first');
55
+ receipts.delete(completed[0]);
56
+ }
57
+ receipts.set(receipt.hash, receipt);
58
+ }
59
+ async complete(chainId, hash, status, error) {
60
+ return this.serialized(async () => {
61
+ const receipts = await this.load(chainId);
62
+ const receipt = receipts.get(hash);
63
+ if (!receipt)
64
+ throw new Error('Receipt disappeared before canonical completion');
65
+ receipt.status = status;
66
+ receipt.error = error;
67
+ await this.save(chainId, receipts);
68
+ });
69
+ }
70
+ async reconcile(receipt) {
71
+ if (typeof this.chain.getBlocks !== 'function')
72
+ throw new Error('Node does not support canonical receipt reconciliation');
73
+ const tip = await this.tip();
74
+ const height = Number(tip?.index ?? 0);
75
+ if (!Number.isSafeInteger(height) || height < 0)
76
+ throw new Error('Invalid canonical height');
77
+ const assertStable = async () => {
78
+ const current = await this.tip();
79
+ if (String(current?.index ?? 0) !== String(tip?.index ?? 0) || current?.hash !== tip?.hash)
80
+ throw new Error('Canonical tip changed during reconciliation; refresh again');
81
+ };
82
+ // Recheck recorded inclusion instead of trusting a previous process's observation.
83
+ if (receipt.blockHeight !== undefined) {
84
+ const block = receipt.blockHeight <= height ? await this.#block(receipt.blockHeight) : undefined;
85
+ if (block?.transactions?.includes(receipt.hash)) {
86
+ await assertStable();
87
+ return { ...receipt, status: 'confirmed', error: undefined, blockHash: block.hash };
88
+ }
89
+ receipt = { ...receipt, status: 'unknown', blockHeight: undefined, blockHash: undefined, scannedHeight: undefined, scannedHash: undefined, startHeight: 0 };
90
+ }
91
+ if (receipt.scannedHeight && receipt.scannedHash) {
92
+ const anchor = receipt.scannedHeight <= height ? await this.#block(receipt.scannedHeight) : undefined;
93
+ if (anchor?.hash !== receipt.scannedHash)
94
+ receipt = { ...receipt, scannedHeight: undefined, scannedHash: undefined, startHeight: 0 };
95
+ }
96
+ // Scan a bounded range per refresh; no pool membership or nonce heuristic proves confirmation.
97
+ const start = Math.max(1, Math.min(receipt.scannedHeight ?? (receipt.startHeight - 1), height) + 1);
98
+ const end = Math.min(height, start + 127);
99
+ let scannedHash = receipt.scannedHash;
100
+ for (let index = start; index <= end; index++) {
101
+ const block = await this.#block(index);
102
+ if (block.transactions.includes(receipt.hash)) {
103
+ await assertStable();
104
+ return { ...receipt, status: 'confirmed', error: undefined, blockHeight: index, blockHash: block.hash, scannedHeight: index, scannedHash: block.hash };
105
+ }
106
+ scannedHash = block.hash;
107
+ }
108
+ await assertStable();
109
+ // Even fully scanned absence is not failure: delivery or execution may still be pending.
110
+ return {
111
+ ...receipt, scannedHeight: Math.max(receipt.scannedHeight ?? 0, end),
112
+ scannedHash,
113
+ status: receipt.status === 'failed' ? 'failed' : 'unknown',
114
+ error: receipt.status === 'failed' ? receipt.error : 'No canonical inclusion observed yet. Refresh to continue reconciliation; do not resend.'
115
+ };
116
+ }
117
+ }
@@ -0,0 +1,2 @@
1
+ /** No remote wallet access: only public queries and client-signed transactions. */
2
+ export declare function managerMiddleware(chain: any): (ctx: any, next: any) => Promise<any>;
@@ -0,0 +1,137 @@
1
+ import { ContractMessage, TransactionMessage } from '@leofcoin/messages';
2
+ import { verifyChainIdentity } from '@leofcoin/lib/chain-identity';
3
+ import { ManagerReceipts } from './manager-receipts.js';
4
+ const errorMessage = (error) => error?.error?.message || error?.message || String(error?.error || error);
5
+ class RequestError extends Error {
6
+ status;
7
+ constructor(status, message) {
8
+ super(message);
9
+ this.status = status;
10
+ }
11
+ }
12
+ /** No remote wallet access: only public queries and client-signed transactions. */
13
+ export function managerMiddleware(chain) {
14
+ const store = globalThis.chainStore;
15
+ const contractStore = globalThis.contractStore;
16
+ const journal = new ManagerReceipts(() => store, chain);
17
+ const live = new Set();
18
+ const peerCount = () => Object.values(globalThis.peernet?.connections || {})
19
+ .filter((peer) => peer.connected).length;
20
+ return async (ctx, next) => {
21
+ if (ctx.path !== '/manager/rpc')
22
+ return next();
23
+ ctx.set('Cache-Control', 'no-store');
24
+ if (ctx.method !== 'POST') {
25
+ ctx.status = 405;
26
+ ctx.body = { error: 'Use POST with application/octet-stream' };
27
+ return;
28
+ }
29
+ try {
30
+ const body = ctx.state.rpcParams;
31
+ if (!body || typeof body !== 'object' || Array.isArray(body))
32
+ throw new RequestError(400, 'Invalid request');
33
+ const identity = await verifyChainIdentity(await chain.getChainIdentity());
34
+ if (body.chainId !== identity.chainId)
35
+ throw new RequestError(409, 'Endpoint chain identity changed; reconnect and review');
36
+ const { action } = body;
37
+ if (action === 'status') {
38
+ const account = body.account;
39
+ if (account !== undefined && (typeof account !== 'string' || account.length > 128))
40
+ throw new RequestError(400, 'Invalid account');
41
+ ctx.body = {
42
+ apiVersion: 1, chainId: identity.chainId, network: identity.network,
43
+ nativeToken: identity.genesisContracts.nativeToken, contractFactory: identity.genesisContracts.contractFactory,
44
+ height: String((await journal.tip())?.index ?? 0), peers: peerCount(), synced: chain.state?.sync === 'synced',
45
+ balance: account ? String(await chain.balanceOf(account) ?? 0) : '0',
46
+ nonce: account ? await chain.getNonce(account) : 0
47
+ };
48
+ }
49
+ else if (action === 'read') {
50
+ if (typeof body.contract !== 'string' || !/^[A-Z2-7]{32,128}$/.test(body.contract) ||
51
+ typeof body.method !== 'string' || !/^[A-Za-z][A-Za-z0-9_]{0,79}$/.test(body.method) ||
52
+ !Array.isArray(body.params) || body.params.length > 100)
53
+ throw new RequestError(400, 'Invalid contract, method or parameter array');
54
+ ctx.body = { value: await chain.staticCall(body.contract, body.method, body.params) ?? null };
55
+ }
56
+ else if (action === 'receipt') {
57
+ if (typeof body.hash !== 'string' || !/^[A-Z2-7]{32,128}$/.test(body.hash))
58
+ throw new RequestError(400, 'Invalid transaction hash');
59
+ ctx.body = await journal.serialized(async () => {
60
+ const receipts = await journal.load(identity.chainId);
61
+ const receipt = receipts.get(body.hash);
62
+ if (!receipt)
63
+ return { hash: body.hash, status: 'unknown' };
64
+ if (live.has(`${identity.chainId}:${body.hash}`))
65
+ return receipt;
66
+ const reconciled = await journal.reconcile(receipt);
67
+ receipts.set(body.hash, reconciled);
68
+ await journal.save(identity.chainId, receipts);
69
+ return reconciled;
70
+ });
71
+ }
72
+ else if (action === 'submit') {
73
+ if (!chain.state || chain.state.sync !== 'synced' || peerCount() === 0)
74
+ throw new RequestError(503, 'Node must be synced and connected before submission');
75
+ const transaction = body.transaction;
76
+ if (!transaction || typeof transaction !== 'object' || !transaction.signature)
77
+ throw new RequestError(400, 'A client-signed transaction is required');
78
+ const message = new TransactionMessage(transaction);
79
+ if (message.encoded.length > 32768)
80
+ throw new RequestError(413, 'Transaction exceeds 32 KiB');
81
+ const hash = await message.hash();
82
+ // Validate before persisting uploaded code, including its binding to this signed registration.
83
+ await chain.validateTransactionSignature(message);
84
+ let contract;
85
+ if (body.contract !== undefined) {
86
+ if (!(body.contract instanceof Uint8Array))
87
+ throw new RequestError(400, 'Contract must be an encoded message');
88
+ contract = new ContractMessage(body.contract);
89
+ if (contract.encoded.length > 262144 || contract.decoded.creator !== transaction.from ||
90
+ transaction.to !== identity.genesisContracts.contractFactory ||
91
+ transaction.method !== 'registerContract' || transaction.params?.length !== 1 ||
92
+ transaction.params[0] !== await contract.hash())
93
+ throw new RequestError(400, 'Contract does not match the signed factory registration');
94
+ }
95
+ ctx.body = await journal.serialized(async () => {
96
+ const receipts = await journal.load(identity.chainId);
97
+ const known = receipts.get(hash);
98
+ if (known)
99
+ return known;
100
+ const height = Number((await journal.tip())?.index ?? 0);
101
+ if (!Number.isSafeInteger(height) || height < 0)
102
+ throw new Error('Invalid submission height');
103
+ const receipt = { hash, status: 'submitting', createdAt: Date.now(), startHeight: height };
104
+ journal.reserve(receipts, receipt);
105
+ await journal.save(identity.chainId, receipts);
106
+ try {
107
+ if (contract)
108
+ await contractStore.put(await contract.hash(), contract.encoded);
109
+ const pending = await chain.sendTransaction(transaction);
110
+ if (pending?.hash !== hash || !pending.wait || typeof pending.wait.then !== 'function')
111
+ throw new Error('Node did not return a canonical completion handle');
112
+ receipt.status = 'pending';
113
+ live.add(`${identity.chainId}:${hash}`);
114
+ Promise.resolve(pending.wait).then(() => journal.complete(identity.chainId, hash, 'confirmed'), error => journal.complete(identity.chainId, hash, 'failed', errorMessage(error))).catch(error => console.error('Could not persist canonical management completion:', hash, error))
115
+ .finally(() => live.delete(`${identity.chainId}:${hash}`));
116
+ }
117
+ catch (error) {
118
+ // The node may already have admitted the transaction. Never silently auto-resend.
119
+ receipt.status = 'unknown';
120
+ receipt.error = errorMessage(error);
121
+ console.error('Management submission could not be verified:', hash, error);
122
+ }
123
+ await journal.save(identity.chainId, receipts);
124
+ return receipt;
125
+ });
126
+ }
127
+ else
128
+ throw new RequestError(400, 'Unsupported management action');
129
+ }
130
+ catch (error) {
131
+ ctx.status = error instanceof RequestError ? error.status : 400;
132
+ ctx.body = { error: errorMessage(error) };
133
+ if (!(error instanceof RequestError))
134
+ console.error('Management request failed:', error);
135
+ }
136
+ };
137
+ }
@@ -0,0 +1,2 @@
1
+ /** Socket route dispatch does not await rejected promises; contain failures here. */
2
+ export declare const guardRpcHandler: (handler: any) => (...args: any[]) => Promise<any>;
@@ -0,0 +1,18 @@
1
+ /** Socket route dispatch does not await rejected promises; contain failures here. */
2
+ export const guardRpcHandler = handler => async (...args) => {
3
+ const index = args.findIndex(value => value && typeof value.send === 'function');
4
+ if (index < 0)
5
+ return;
6
+ const response = args[index];
7
+ const send = (value, status = 200) => response.send(value ?? null, status);
8
+ args[index] = { ...response, send };
9
+ try {
10
+ return await handler(...args);
11
+ }
12
+ catch (error) {
13
+ try {
14
+ send({ error: error instanceof Error ? error.message : 'RPC operation failed' }, 422);
15
+ }
16
+ catch { /* The socket may already be closed; never reject the detached route. */ }
17
+ }
18
+ };
package/exports/ws.d.ts CHANGED
@@ -1,5 +1,2 @@
1
- declare const _default: (chain: any, port: any, networkVersion: any, remote?: boolean) => Promise<{
2
- close: () => void;
3
- connections: import("socket-request-server/connection").SocketRequestConnection[];
4
- }>;
1
+ declare const _default: (chain: any, port: any, networkVersion: any, remote?: boolean) => any;
5
2
  export default _default;
package/exports/ws.js CHANGED
@@ -1,8 +1,9 @@
1
- import Server from 'socket-request-server';
1
+ import Server from './binary-socket.js';
2
2
  import { formatUnits } from '@leofcoin/utils';
3
3
  import shared from './shared.js';
4
+ import { guardRpcHandler } from './rpc-handler.js';
4
5
  export default (chain, port, networkVersion, remote = false) => {
5
- return Server({ port, protocol: networkVersion }, {
6
+ const routes = {
6
7
  network: async ({ send }) => {
7
8
  send({ version: networkVersion });
8
9
  },
@@ -50,19 +51,19 @@ export default (chain, port, networkVersion, remote = false) => {
50
51
  sendTransaction: async (transaction, { send }) => {
51
52
  try {
52
53
  const tx = await chain.sendTransaction(transaction);
53
- await tx.wait();
54
+ await (typeof tx.wait === 'function' ? tx.wait() : tx.wait);
54
55
  delete tx.wait;
55
56
  send(tx);
56
57
  }
57
58
  catch (error) {
58
- send(202);
59
+ send({ error: error instanceof Error ? error.message : 'Transaction failed' }, 422);
59
60
  }
60
61
  },
61
62
  peerId: ({ send }) => send(globalThis.peernet.peerId),
62
63
  peers: ({ send }) => send(globalThis.peernet.peers.map(([id, peer]) => id)),
63
64
  validators: ({ send }) => send(chain.validators),
64
65
  lookup: async ({ name }, { send }) => send(await chain.lookup(name)),
65
- staticCall: async ({ contract, method, params }, { send }) => send(await chain.staticCall(contract, method, params)),
66
+ staticCall: async ({ contract, method, params }, { send }) => send(await chain.staticCall(contract, method, params ?? [])),
66
67
  contracts: async ({ send }) => send(await chain.contracts),
67
68
  totalContracts: async ({ send }) => send(await chain.totalContracts),
68
69
  nativeToken: async ({ send }) => send(await chain.nativeToken),
@@ -93,12 +94,12 @@ export default (chain, port, networkVersion, remote = false) => {
93
94
  deployContract: async ({ transaction, contract }, { send }) => {
94
95
  try {
95
96
  const tx = await chain.deployContract(contract, transaction);
96
- await tx.wait();
97
+ await (typeof tx.wait === 'function' ? tx.wait() : tx.wait);
97
98
  delete tx.wait;
98
99
  send(tx);
99
100
  }
100
101
  catch (error) {
101
- send(error.message);
102
+ send({ error: error instanceof Error ? error.message : 'Deployment failed' }, 422);
102
103
  }
103
104
  },
104
105
  createContractAddress: async ({ owner, code, params }, { send }) => {
@@ -107,12 +108,13 @@ export default (chain, port, networkVersion, remote = false) => {
107
108
  send(address);
108
109
  }
109
110
  catch (error) {
110
- send(error);
111
+ send({ error: error instanceof Error ? error.message : 'Contract address failed' }, 422);
111
112
  }
112
113
  },
113
114
  blockHashMap: async ({ send }) => send(await chain.blockHashMap),
114
115
  bootstrap: async ({ send }) => {
115
116
  send(await shared.bootstrap());
116
117
  }
117
- });
118
+ };
119
+ return Server({ port, protocol: networkVersion }, Object.fromEntries(Object.entries(routes).map(([name, handler]) => [name, guardRpcHandler(handler)])));
118
120
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leofcoin/endpoints",
3
- "version": "0.4.11",
3
+ "version": "0.5.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "exports": {
@@ -34,12 +34,15 @@
34
34
  },
35
35
  "homepage": "https://github.com/leofcoin/monorepo/tree/main/packages/endpoints#readme",
36
36
  "dependencies": {
37
+ "@leofcoin/binary": "^1.1.0",
38
+ "ws": "^8.22.0",
39
+ "@leofcoin/lib": "^1.4.0",
40
+ "@leofcoin/messages": "^2.0.0",
37
41
  "@koa/cors": "^5.0.0",
38
42
  "@koa/router": "^13.1.1",
39
43
  "@leofcoin/utils": "^1.1.45",
40
44
  "koa": "^3.0.1",
41
- "showdown": "^2.1.0",
42
- "socket-request-server": "^1.6.17"
45
+ "showdown": "^2.1.0"
43
46
  },
44
47
  "devDependencies": {
45
48
  "@leofcoin/types": "^1.0.22",