@leofcoin/endpoints 0.4.11 → 0.4.12
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 +55 -1
- package/exports/http.js +2 -0
- package/exports/manager-receipts.d.ts +43 -0
- package/exports/manager-receipts.js +117 -0
- package/exports/manager.d.ts +2 -0
- package/exports/manager.js +156 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -6,4 +6,58 @@ http(chain, port, networkVersion)
|
|
|
6
6
|
|
|
7
7
|
```js
|
|
8
8
|
wss(chain, port, networkVersion)
|
|
9
|
-
```
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Chain Manager HTTP API
|
|
12
|
+
|
|
13
|
+
`POST /manager/rpc` accepts `application/json` (maximum 1 MiB). Requests must
|
|
14
|
+
include the full expected `chainId`; the node verifies its own genesis identity
|
|
15
|
+
and returns HTTP 409 on a mismatch. Existing endpoints remain unchanged.
|
|
16
|
+
This API never unlocks a node wallet, selects remote accounts or signs messages.
|
|
17
|
+
Deploy/restart an updated endpoints package before connecting Chain Manager.
|
|
18
|
+
|
|
19
|
+
Actions:
|
|
20
|
+
|
|
21
|
+
| Action | Additional fields | Result |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `status` | optional public `account` | API version, chain identity, sync, peer count, height, contract hashes, account balance in integer units and latest nonce |
|
|
24
|
+
| `read` | `contract`, `method`, JSON array `params` | `{ value }`, using the existing bigint JSON codec |
|
|
25
|
+
| `submit` | signed `transaction`, optional encoded `contract` | transaction hash and explicit status |
|
|
26
|
+
| `receipt` | transaction `hash` | durable status, canonical inclusion (`blockHeight`, `blockHash`) and scan progress (`scannedHeight`) when observed, or `unknown` |
|
|
27
|
+
|
|
28
|
+
Submissions require a synced node with connected peers. A contract upload must
|
|
29
|
+
match the signed factory registration, creator and content hash. The node
|
|
30
|
+
validates signatures before storing uploaded code. Duplicate transaction hashes
|
|
31
|
+
are admitted once, including concurrent duplicates.
|
|
32
|
+
|
|
33
|
+
`pending` means admitted, not confirmed. The chain's canonical completion
|
|
34
|
+
promise or verified canonical block membership produces `confirmed`; execution
|
|
35
|
+
rejection produces `failed`. Unverifiable admission
|
|
36
|
+
produces `unknown`, never a success-shaped response or an automatic resend.
|
|
37
|
+
|
|
38
|
+
Before any contract upload/admission, a reservation is durably saved in the
|
|
39
|
+
node's existing isolated `chainStore`, under `manager:receipts:v1:<full-chain-id>`.
|
|
40
|
+
Storage unavailability/corruption/write failure blocks admission explicitly.
|
|
41
|
+
Updates and duplicate checks are serialized. Restarting the HTTP server/node
|
|
42
|
+
restores admission history and does not resubmit a reserved transaction.
|
|
43
|
+
|
|
44
|
+
After restart, `receipt` scans at most 128 canonical blocks per request from the
|
|
45
|
+
submission height, persists its cursor and checks recorded inclusion/scan anchors.
|
|
46
|
+
It uses canonical worker blocks, not the mempool or raw transaction store; absent
|
|
47
|
+
inclusion is `unknown`, never inferred failure. Refresh again to continue a long
|
|
48
|
+
scan. A changing tip or unavailable canonical block returns an explicit error
|
|
49
|
+
without advancing durable progress. Canonical tip lookup avoids cached fallback
|
|
50
|
+
state when the machine exposes its strict getter.
|
|
51
|
+
|
|
52
|
+
Receipts are bounded to 512 entries per chain. Only completed entries may be
|
|
53
|
+
evicted; a full unresolved journal blocks new admission. Reservations written
|
|
54
|
+
before a crash can remain unknown if delivery never happened. Evicted entries,
|
|
55
|
+
pre-upgrade transactions and receipts from another node are also unknown; these
|
|
56
|
+
require operator reconciliation and are not proof of rejection. The journal is
|
|
57
|
+
local to a node, not shared across distinct node instances.
|
|
58
|
+
|
|
59
|
+
Use HTTPS and an operator-controlled endpoint. CORS permits the web and Electron
|
|
60
|
+
clients to connect but is not authentication. The API only accepts transactions
|
|
61
|
+
already signed by the client; it provides no privileged server-side keys.
|
|
62
|
+
Chain-ID checks protect this connection from accidental chain switching; they do
|
|
63
|
+
not add cryptographic cross-chain replay protection to the existing wire format.
|
package/exports/http.js
CHANGED
|
@@ -7,6 +7,7 @@ import Showdown from 'showdown';
|
|
|
7
7
|
import shared from './shared.js';
|
|
8
8
|
import { dirname, join } from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
|
+
import { managerMiddleware } from './manager.js';
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = dirname(__filename);
|
|
12
13
|
const converter = new Showdown.Converter();
|
|
@@ -155,6 +156,7 @@ export default (chain, port, networkVersion, remote = false) => {
|
|
|
155
156
|
ctx.type = 'application/json';
|
|
156
157
|
}
|
|
157
158
|
});
|
|
159
|
+
api.use(managerMiddleware(chain));
|
|
158
160
|
api.use(router.routes());
|
|
159
161
|
return api.listen(port);
|
|
160
162
|
};
|
|
@@ -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,156 @@
|
|
|
1
|
+
import { ContractMessage, TransactionMessage } from '@leofcoin/messages';
|
|
2
|
+
import { verifyChainIdentity } from '@leofcoin/lib/chain-identity';
|
|
3
|
+
import { ManagerReceipts } from './manager-receipts.js';
|
|
4
|
+
const MAX_BODY = 1_048_576;
|
|
5
|
+
const errorMessage = (error) => error?.error?.message || error?.message || String(error?.error || error);
|
|
6
|
+
class RequestError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
constructor(status, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = status;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function readBody(request) {
|
|
14
|
+
let size = 0;
|
|
15
|
+
const chunks = [];
|
|
16
|
+
for await (const chunk of request) {
|
|
17
|
+
size += chunk.length;
|
|
18
|
+
if (size > MAX_BODY)
|
|
19
|
+
throw new RequestError(413, 'Management request exceeds 1 MiB');
|
|
20
|
+
chunks.push(chunk);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new RequestError(400, 'Management request must contain JSON');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** No remote wallet access: only public queries and client-signed transactions. */
|
|
30
|
+
export function managerMiddleware(chain) {
|
|
31
|
+
const store = globalThis.chainStore;
|
|
32
|
+
const contractStore = globalThis.contractStore;
|
|
33
|
+
const journal = new ManagerReceipts(() => store, chain);
|
|
34
|
+
const live = new Set();
|
|
35
|
+
const peerCount = () => Object.values(globalThis.peernet?.connections || {})
|
|
36
|
+
.filter((peer) => peer.connected).length;
|
|
37
|
+
return async (ctx, next) => {
|
|
38
|
+
if (ctx.path !== '/manager/rpc')
|
|
39
|
+
return next();
|
|
40
|
+
ctx.set('Cache-Control', 'no-store');
|
|
41
|
+
if (ctx.method !== 'POST') {
|
|
42
|
+
ctx.status = 405;
|
|
43
|
+
ctx.body = { error: 'Use POST with application/json' };
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
if (!ctx.is('application/json'))
|
|
48
|
+
throw new RequestError(415, 'Content-Type must be application/json');
|
|
49
|
+
const body = await readBody(ctx.req);
|
|
50
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
51
|
+
throw new RequestError(400, 'Invalid request');
|
|
52
|
+
const identity = await verifyChainIdentity(await chain.getChainIdentity());
|
|
53
|
+
if (body.chainId !== identity.chainId)
|
|
54
|
+
throw new RequestError(409, 'Endpoint chain identity changed; reconnect and review');
|
|
55
|
+
const { action } = body;
|
|
56
|
+
if (action === 'status') {
|
|
57
|
+
const account = body.account;
|
|
58
|
+
if (account !== undefined && (typeof account !== 'string' || account.length > 128))
|
|
59
|
+
throw new RequestError(400, 'Invalid account');
|
|
60
|
+
ctx.body = {
|
|
61
|
+
apiVersion: 1, chainId: identity.chainId, network: identity.network,
|
|
62
|
+
nativeToken: identity.genesisContracts.nativeToken, contractFactory: identity.genesisContracts.contractFactory,
|
|
63
|
+
height: String((await journal.tip())?.index ?? 0), peers: peerCount(), synced: chain.state?.sync === 'synced',
|
|
64
|
+
balance: account ? String(await chain.balanceOf(account) ?? 0) : '0',
|
|
65
|
+
nonce: account ? await chain.getNonce(account) : 0
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
else if (action === 'read') {
|
|
69
|
+
if (typeof body.contract !== 'string' || !/^[A-Z2-7]{32,128}$/.test(body.contract) ||
|
|
70
|
+
typeof body.method !== 'string' || !/^[A-Za-z][A-Za-z0-9_]{0,79}$/.test(body.method) ||
|
|
71
|
+
!Array.isArray(body.params) || body.params.length > 100)
|
|
72
|
+
throw new RequestError(400, 'Invalid contract, method or parameter array');
|
|
73
|
+
ctx.body = { value: await chain.staticCall(body.contract, body.method, body.params) ?? null };
|
|
74
|
+
}
|
|
75
|
+
else if (action === 'receipt') {
|
|
76
|
+
if (typeof body.hash !== 'string' || !/^[A-Z2-7]{32,128}$/.test(body.hash))
|
|
77
|
+
throw new RequestError(400, 'Invalid transaction hash');
|
|
78
|
+
ctx.body = await journal.serialized(async () => {
|
|
79
|
+
const receipts = await journal.load(identity.chainId);
|
|
80
|
+
const receipt = receipts.get(body.hash);
|
|
81
|
+
if (!receipt)
|
|
82
|
+
return { hash: body.hash, status: 'unknown' };
|
|
83
|
+
if (live.has(`${identity.chainId}:${body.hash}`))
|
|
84
|
+
return receipt;
|
|
85
|
+
const reconciled = await journal.reconcile(receipt);
|
|
86
|
+
receipts.set(body.hash, reconciled);
|
|
87
|
+
await journal.save(identity.chainId, receipts);
|
|
88
|
+
return reconciled;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else if (action === 'submit') {
|
|
92
|
+
if (!chain.state || chain.state.sync !== 'synced' || peerCount() === 0)
|
|
93
|
+
throw new RequestError(503, 'Node must be synced and connected before submission');
|
|
94
|
+
const transaction = body.transaction;
|
|
95
|
+
if (!transaction || typeof transaction !== 'object' || !transaction.signature)
|
|
96
|
+
throw new RequestError(400, 'A client-signed transaction is required');
|
|
97
|
+
const message = new TransactionMessage(transaction);
|
|
98
|
+
if (message.encoded.length > 32768)
|
|
99
|
+
throw new RequestError(413, 'Transaction exceeds 32 KiB');
|
|
100
|
+
const hash = await message.hash();
|
|
101
|
+
// Validate before persisting uploaded code, including its binding to this signed registration.
|
|
102
|
+
await chain.validateTransactionSignature(message);
|
|
103
|
+
let contract;
|
|
104
|
+
if (body.contract !== undefined) {
|
|
105
|
+
if (typeof body.contract !== 'string')
|
|
106
|
+
throw new RequestError(400, 'Contract must be an encoded message');
|
|
107
|
+
contract = new ContractMessage(body.contract);
|
|
108
|
+
if (contract.encoded.length > 262144 || contract.decoded.creator !== transaction.from ||
|
|
109
|
+
transaction.to !== identity.genesisContracts.contractFactory ||
|
|
110
|
+
transaction.method !== 'registerContract' || transaction.params?.length !== 1 ||
|
|
111
|
+
transaction.params[0] !== await contract.hash())
|
|
112
|
+
throw new RequestError(400, 'Contract does not match the signed factory registration');
|
|
113
|
+
}
|
|
114
|
+
ctx.body = await journal.serialized(async () => {
|
|
115
|
+
const receipts = await journal.load(identity.chainId);
|
|
116
|
+
const known = receipts.get(hash);
|
|
117
|
+
if (known)
|
|
118
|
+
return known;
|
|
119
|
+
const height = Number((await journal.tip())?.index ?? 0);
|
|
120
|
+
if (!Number.isSafeInteger(height) || height < 0)
|
|
121
|
+
throw new Error('Invalid submission height');
|
|
122
|
+
const receipt = { hash, status: 'submitting', createdAt: Date.now(), startHeight: height };
|
|
123
|
+
journal.reserve(receipts, receipt);
|
|
124
|
+
await journal.save(identity.chainId, receipts);
|
|
125
|
+
try {
|
|
126
|
+
if (contract)
|
|
127
|
+
await contractStore.put(await contract.hash(), contract.encoded);
|
|
128
|
+
const pending = await chain.sendTransaction(transaction);
|
|
129
|
+
if (pending?.hash !== hash || !pending.wait || typeof pending.wait.then !== 'function')
|
|
130
|
+
throw new Error('Node did not return a canonical completion handle');
|
|
131
|
+
receipt.status = 'pending';
|
|
132
|
+
live.add(`${identity.chainId}:${hash}`);
|
|
133
|
+
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))
|
|
134
|
+
.finally(() => live.delete(`${identity.chainId}:${hash}`));
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// The node may already have admitted the transaction. Never silently auto-resend.
|
|
138
|
+
receipt.status = 'unknown';
|
|
139
|
+
receipt.error = errorMessage(error);
|
|
140
|
+
console.error('Management submission could not be verified:', hash, error);
|
|
141
|
+
}
|
|
142
|
+
await journal.save(identity.chainId, receipts);
|
|
143
|
+
return receipt;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else
|
|
147
|
+
throw new RequestError(400, 'Unsupported management action');
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
ctx.status = error instanceof RequestError ? error.status : 400;
|
|
151
|
+
ctx.body = { error: errorMessage(error) };
|
|
152
|
+
if (!(error instanceof RequestError))
|
|
153
|
+
console.error('Management request failed:', error);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/endpoints",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"homepage": "https://github.com/leofcoin/monorepo/tree/main/packages/endpoints#readme",
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@leofcoin/lib": "^1.3.15",
|
|
38
|
+
"@leofcoin/messages": "^1.6.5",
|
|
37
39
|
"@koa/cors": "^5.0.0",
|
|
38
40
|
"@koa/router": "^13.1.1",
|
|
39
41
|
"@leofcoin/utils": "^1.1.45",
|