sememe 0.0.0-stage → 0.4.21
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/LICENSE +21 -0
- package/README.md +2667 -2
- package/bin/sememe.mjs +556 -0
- package/index.mjs +48 -0
- package/interface/ata-ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL.json +62 -0
- package/interface/native-11111111111111111111111111111111.json +226 -0
- package/interface/native-ComputeBudget111111111111111111111111111111.json +72 -0
- package/interface/raydium-675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8.json +352 -0
- package/interface/token-TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.json +334 -0
- package/interface/token-TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.json +535 -0
- package/interface/token-metadata.json +132 -0
- package/package.json +71 -4
- package/src/account-list.mjs +308 -0
- package/src/account-union.mjs +523 -0
- package/src/account.mjs +199 -0
- package/src/acquire.mjs +145 -0
- package/src/adopt.mjs +205 -0
- package/src/ata-interface.mjs +276 -0
- package/src/ata.mjs +223 -0
- package/src/base58.mjs +96 -0
- package/src/canonical.mjs +24 -0
- package/src/cli-surface.mjs +300 -0
- package/src/codec.mjs +1069 -0
- package/src/conformance.mjs +431 -0
- package/src/corpus/harvest-compute-budget-2026-10-31.json +19122 -0
- package/src/corpus/harvest-system-2026-10-31.json +26090 -0
- package/src/decode.mjs +286 -0
- package/src/dialect.mjs +145 -0
- package/src/discriminators.mjs +177 -0
- package/src/drift.mjs +172 -0
- package/src/gap.mjs +205 -0
- package/src/lighthouse.mjs +168 -0
- package/src/lock.mjs +440 -0
- package/src/native-audit.mjs +56 -0
- package/src/native-corpus.mjs +57 -0
- package/src/native-interface.mjs +252 -0
- package/src/native.mjs +324 -0
- package/src/peer.mjs +95 -0
- package/src/program-id.mjs +74 -0
- package/src/pull.mjs +117 -0
- package/src/rawle.mjs +450 -0
- package/src/raydium-interface.mjs +747 -0
- package/src/raydium.mjs +391 -0
- package/src/receipt-digest.mjs +84 -0
- package/src/receipt-name.mjs +128 -0
- package/src/receipt-order.mjs +56 -0
- package/src/shank.mjs +73 -0
- package/src/short-forms.mjs +209 -0
- package/src/store-read.mjs +250 -0
- package/src/store-write.mjs +46 -0
- package/src/token-interface.mjs +394 -0
- package/src/token-metadata-interface.mjs +371 -0
- package/src/token-metadata.mjs +317 -0
- package/src/token.mjs +443 -0
- package/src/union.mjs +428 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/LICENSE.txt +201 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/compute-budget-interface/src/lib.rs +93 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/system-interface/src/instruction.rs +1899 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/LICENSE +21 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/lighthouse.json +2769 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/LICENSE +201 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/instruction.rs +1189 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/state.rs +1348 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/LICENSE +201 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/instruction.rs +169 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/lib.rs +12 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/program/src/processor.rs +308 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/LICENSE.txt +176 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/interface/src/instruction.rs +1963 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/LICENSE.txt +176 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/interface/src/instruction.rs +2868 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/LICENSE +201 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/error.rs +58 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/instruction.rs +596 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/lib.rs +21 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/state.rs +229 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Is this string a program id? A DEPENDENCY-FREE check.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS FILE EXISTS
|
|
4
|
+
//
|
|
5
|
+
// `sememe conform --only <program-id>` filters a transaction's instructions
|
|
6
|
+
// to one program. A filter that cannot match anything must be REFUSED by
|
|
7
|
+
// name, not silently honoured (measured 2026-11-09, published v0.4.12): a
|
|
8
|
+
// filter that named a program the transaction never invokes produced a
|
|
9
|
+
// report with zero instructions, zero disagreements and exit 0 — a green
|
|
10
|
+
// CI gate over nothing.
|
|
11
|
+
//
|
|
12
|
+
// The first half of that defect is a filter that is not even a program id:
|
|
13
|
+
// `--only not-a-program-id` can never match, and it produced the same green
|
|
14
|
+
// report. Refusing it needs a definition of "program id", and the definition
|
|
15
|
+
// must not drag in the acquisition peer: the decode core is imported by
|
|
16
|
+
// consumers with NO npm dependencies at all (pinned by test/deps.test.mjs),
|
|
17
|
+
// and a CLI refusal must not be the thing that makes them install
|
|
18
|
+
// @solana/web3.js.
|
|
19
|
+
//
|
|
20
|
+
// A Solana program id is 32 raw bytes in base58. That is exactly what
|
|
21
|
+
// `src/base58.mjs` already encodes and decodes with no dependencies, so
|
|
22
|
+
// "is this a program id" is answered here as:
|
|
23
|
+
//
|
|
24
|
+
// 1. it decodes as base58 at all;
|
|
25
|
+
// 2. it decodes to exactly 32 bytes;
|
|
26
|
+
// 3. it RE-ENCODES to the same string (canonical form — a base58 string
|
|
27
|
+
// with leading zero bytes has several spellings, and a filter that
|
|
28
|
+
// names a program in a non-canonical spelling would silently match
|
|
29
|
+
// nothing while looking perfectly valid).
|
|
30
|
+
//
|
|
31
|
+
// The same check is used by the pull path through src/peer.mjs's PublicKey;
|
|
32
|
+
// this file is the dependency-free twin for the paths that must not load the
|
|
33
|
+
// peer. The two agree on what a canonical program id is — one round-trip
|
|
34
|
+
// through base58 is the same arithmetic PublicKey performs.
|
|
35
|
+
import { decodeBase58, encodeBase58 } from './base58.mjs';
|
|
36
|
+
|
|
37
|
+
/** A Solana program id is 32 bytes. */
|
|
38
|
+
export const PROGRAM_ID_BYTES = 32;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Is `value` a canonical base58 program id?
|
|
42
|
+
*
|
|
43
|
+
* @param {unknown} value
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
export function isProgramId(value) {
|
|
47
|
+
if (typeof value !== 'string') return false;
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
if (!trimmed) return false;
|
|
50
|
+
let bytes;
|
|
51
|
+
try {
|
|
52
|
+
bytes = decodeBase58(trimmed);
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (bytes.length !== PROGRAM_ID_BYTES) return false;
|
|
57
|
+
try {
|
|
58
|
+
return encodeBase58(bytes) === trimmed;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The refusal text for a filter that is not a program id.
|
|
66
|
+
*
|
|
67
|
+
* The message names the FLAG and the VALUE, because a caller who typed the
|
|
68
|
+
* wrong thing must be able to see what was read: "unknown value" without the
|
|
69
|
+
* value is a verdict, not a repair.
|
|
70
|
+
*/
|
|
71
|
+
export function notAProgramIdMessage(flag, value) {
|
|
72
|
+
return `sememe conform: ${flag} ${JSON.stringify(value)} is not a program id — `
|
|
73
|
+
+ 'it names 32 bytes in base58, and can never match an instruction';
|
|
74
|
+
}
|
package/src/pull.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// First sememe capability: pull a Solana program IDL into a local content-addressed store.
|
|
2
|
+
// No decoding of transactions yet; no network call after the pull is stored.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { link, lstat, mkdir, open, readFile, rm } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
// The optional peer, loaded so its ABSENCE is sememe's sentence instead of a
|
|
7
|
+
// Node loader stack trace (src/peer.mjs; measured 2026-11-06 from a bare
|
|
8
|
+
// tarball install without the peer).
|
|
9
|
+
import { PublicKey } from './peer.mjs';
|
|
10
|
+
// Acquisition helpers live IN src/ since 2026-11-01: pull used to import
|
|
11
|
+
// `../tools/measure.mjs`, which broke any consumer installing this package —
|
|
12
|
+
// the file simply would not exist inside node_modules/sememe/src.
|
|
13
|
+
import { addresses, decodeAccount, rpcAccounts } from './acquire.mjs';
|
|
14
|
+
|
|
15
|
+
// Re-exported from its own module so a consumer can verify a fingerprint
|
|
16
|
+
// without importing this file (and therefore without @solana/web3.js).
|
|
17
|
+
export { canonicalJson } from './canonical.mjs';
|
|
18
|
+
import { canonicalJson } from './canonical.mjs';
|
|
19
|
+
|
|
20
|
+
const sha256 = (data) => createHash('sha256').update(data).digest('hex');
|
|
21
|
+
const validId = (input) => {
|
|
22
|
+
if (typeof input !== 'string') throw Error('program id must be a string');
|
|
23
|
+
const pk = new PublicKey(input);
|
|
24
|
+
if (pk.toBase58() !== input) throw Error('noncanonical program id');
|
|
25
|
+
return pk.toBase58();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// This fingerprint describes JSON content, not freshness, chain authenticity, or ABI parity.
|
|
29
|
+
// The canonical form itself lives in src/canonical.mjs (dependency-free);
|
|
30
|
+
// re-exported above for callers that import it from here.
|
|
31
|
+
|
|
32
|
+
async function writeOnce(destination, content) {
|
|
33
|
+
try {
|
|
34
|
+
const entry = await lstat(destination);
|
|
35
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw Error(`non-regular store entry: ${destination}`);
|
|
36
|
+
const current = await readFile(destination);
|
|
37
|
+
if (!current.equals(content)) throw Error(`digest collision or corrupted local object: ${destination}`);
|
|
38
|
+
return;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
41
|
+
}
|
|
42
|
+
const temporary = `${destination}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
|
|
43
|
+
let handle;
|
|
44
|
+
try {
|
|
45
|
+
handle = await open(temporary, 'wx', 0o600);
|
|
46
|
+
await handle.writeFile(content);
|
|
47
|
+
await handle.sync();
|
|
48
|
+
await handle.close(); handle = null;
|
|
49
|
+
// Atomic no-replace installation: hard-link fails if another writer won.
|
|
50
|
+
// Rename would overwrite a concurrently created or symlinked digest path.
|
|
51
|
+
try { await link(temporary, destination); }
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
54
|
+
const entry = await lstat(destination);
|
|
55
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw Error(`non-regular store entry: ${destination}`);
|
|
56
|
+
const current = await readFile(destination);
|
|
57
|
+
if (!current.equals(content)) throw Error(`digest collision or corrupted local object: ${destination}`);
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
if (handle) await handle.close();
|
|
61
|
+
await rm(temporary, { force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Install one verified on-chain IDL into the store: objects/ + receipts/.
|
|
67
|
+
*
|
|
68
|
+
* Extracted from `pull` on 2026-11-04 so a batch acquisition over MANY
|
|
69
|
+
* programs shares exactly one write path with the single-program CLI: a
|
|
70
|
+
* second writer would drift (the step-ingestooor sweep measured 34 pullable
|
|
71
|
+
* programs; pulling them one process each re-probes and re-hashes what one
|
|
72
|
+
* batched getMultipleAccounts already answered). The arguments are exactly
|
|
73
|
+
* what `pull` produced after its verification: the decoded IDL, the source
|
|
74
|
+
* that answered, its address, and the slot the RPC context carried.
|
|
75
|
+
*/
|
|
76
|
+
export async function installIdl(idl, { program, source, address, slot }, storeDir) {
|
|
77
|
+
const json = Buffer.from(canonicalJson(idl) + '\n');
|
|
78
|
+
const fingerprint = sha256(json);
|
|
79
|
+
if (!Number.isSafeInteger(slot) || slot < 0) throw Error('RPC context lacks a valid slot');
|
|
80
|
+
const directory = path.resolve(storeDir);
|
|
81
|
+
const objects = path.join(directory, 'objects');
|
|
82
|
+
const receipts = path.join(directory, 'receipts');
|
|
83
|
+
await mkdir(objects, { recursive: true, mode: 0o700 });
|
|
84
|
+
await mkdir(receipts, { recursive: true, mode: 0o700 });
|
|
85
|
+
const file = path.join(objects, `${fingerprint}.json`);
|
|
86
|
+
await writeOnce(file, json);
|
|
87
|
+
const receipt = {
|
|
88
|
+
program, source, address, slot,
|
|
89
|
+
fingerprint: `sha256:${fingerprint}`, instructionCount: idl.instructions.length,
|
|
90
|
+
};
|
|
91
|
+
const receiptPath = path.join(receipts, `${program}-${slot}-${source}-${fingerprint}.json`);
|
|
92
|
+
await writeOnce(receiptPath, Buffer.from(canonicalJson(receipt) + '\n'));
|
|
93
|
+
return { ...receipt, path: file, receiptPath };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function pull(programInput, storeDir, rpcUrl = 'https://api.mainnet-beta.solana.com', fetcher = fetch) {
|
|
97
|
+
const program = validId(programInput);
|
|
98
|
+
if (typeof storeDir !== 'string' || !storeDir.trim()) throw Error('explicit --store path required');
|
|
99
|
+
if (!/^https:\/\//.test(rpcUrl)) throw Error('HTTPS RPC URL required');
|
|
100
|
+
const { metadata, anchor } = await addresses(program);
|
|
101
|
+
const response = await rpcAccounts(rpcUrl, [metadata, anchor], fetcher, 3);
|
|
102
|
+
if (response.error) throw Error(`RPC unavailable: ${response.error}`);
|
|
103
|
+
let selected, idl;
|
|
104
|
+
const errors = [];
|
|
105
|
+
for (const [source, address, account] of [
|
|
106
|
+
['metadata', metadata, response.accounts[0]], ['anchor', anchor, response.accounts[1]],
|
|
107
|
+
]) {
|
|
108
|
+
if (!account) { errors.push(`${source}: account missing`); continue; }
|
|
109
|
+
try {
|
|
110
|
+
idl = decodeAccount(account, source, program);
|
|
111
|
+
selected = { source, address };
|
|
112
|
+
break;
|
|
113
|
+
} catch (error) { errors.push(`${source}: ${String(error.message).slice(0, 100)}`); }
|
|
114
|
+
}
|
|
115
|
+
if (!selected) throw Error(`no verified on-chain IDL for ${program}: ${errors.join('; ')}`);
|
|
116
|
+
return installIdl(idl, { program, source: selected.source, address: selected.address, slot: response.slot }, storeDir);
|
|
117
|
+
}
|
package/src/rawle.mjs
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
// src/rawle.mjs — the raw little-endian tag-dispatch dialect ENGINE.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: 12.11. PULS X named the refusal for interfaces sememe
|
|
4
|
+
// does not speak (src/dialect.mjs) — the right first step, but Benennen is
|
|
5
|
+
// NICHT Support. The measured face: DiabLokxisGR8P4Qqp2QL6PmBhhVsdzPstCE4
|
|
6
|
+
// WoLxc5z (cryptodiablo/diabloarb, first-party IDL at pin f5005ac4) — 102
|
|
7
|
+
// payloads in the committed ecosystem sample, 0 decoded: the dialect gate
|
|
8
|
+
// refuses its whole file. The refusal was honest because the grammar
|
|
9
|
+
// carries no Anchor/shank structure the codec could read: `types` is a
|
|
10
|
+
// dict, instructions dispatch on a 1-byte `tag`, and list lengths are u8
|
|
11
|
+
// COUNTERS in the header, not Borsh u32 prefixes.
|
|
12
|
+
//
|
|
13
|
+
// What this file is: an engine for exactly the shape the interface
|
|
14
|
+
// DECLARES it speaks — `encoding: "little-endian; u8/u16/u32/u64 unsigned,
|
|
15
|
+
// i64 signed; no padding; no length prefixes except the u8 counters named
|
|
16
|
+
// in each header"` (the file's own words). It decodes the declared field
|
|
17
|
+
// list over the bytes, and encodes the field list back. Byte-exactness of
|
|
18
|
+
// decode -> encode is the only evidence it offers, and the measurement
|
|
19
|
+
// (tools/measure-rawle-decoder.mjs) holds it against the REAL wire of all
|
|
20
|
+
// 102 committed payloads: anything the fixed grammar cannot explain exits
|
|
21
|
+
// as a named refusal, never a guess.
|
|
22
|
+
//
|
|
23
|
+
// The grammar it accepts (and NOTHING else):
|
|
24
|
+
// - instruction dispatch: a `tag` (0..255) plus a `data` field list whose
|
|
25
|
+
// first entry is { name: 'tag', type: 'u8', value: <tag> }.
|
|
26
|
+
// - primitives: u8, u16, u32, u64 (unsigned), i64 (signed) — exactly the
|
|
27
|
+
// set the declared encoding names. Anything else (string, bytes,
|
|
28
|
+
// pubkey, bool, floats, i8/i16/i32/i128, u128) is OUT: a dialect the
|
|
29
|
+
// file does not declare is not this dialect.
|
|
30
|
+
// - named types: `types` as a DICT keyed by name; each entry is a field
|
|
31
|
+
// list, optionally with a declared fixed `size` that MUST equal the
|
|
32
|
+
// sum of its fields (measured: Hop size 5 = 5x u8, Base 24 = 3x u64) —
|
|
33
|
+
// a size that disagrees is a grammar the engine will not pretend to
|
|
34
|
+
// read.
|
|
35
|
+
// - length-prefixed-by-counter arrays: `Type[n_field]` where `n_field` is
|
|
36
|
+
// a preceding u8 primitive field in the SAME field list (measured:
|
|
37
|
+
// Base[n_bases], Hop[n_windows], Route[n_routes], u8[n_hops]).
|
|
38
|
+
// - one trailing OPTIONAL field: `optional: <prose>` on the LAST field
|
|
39
|
+
// only. The prose (measured: "present only with SearchFlags.BUDGET") is
|
|
40
|
+
// not machine-readable, so presence is decided BY THE BYTES: the option
|
|
41
|
+
// field is read exactly when its full size fits the remainder to zero.
|
|
42
|
+
// A remainder that is neither exactly the option size nor zero is a
|
|
43
|
+
// named refusal (trailing bytes the layout does not explain).
|
|
44
|
+
//
|
|
45
|
+
// NOT WIRED INTO THE PRODUCT. No call site in codec/union/adopt/store-read
|
|
46
|
+
// or the CLI routes through this engine yet — PULS X's named refusal stays
|
|
47
|
+
// the product's answer at every boundary, byte for byte, and the dialect
|
|
48
|
+
// gate is untouched. That is deliberate: making support the product answer
|
|
49
|
+
// changes 19 pinned faces of the PULS X measurement (its artifacts and
|
|
50
|
+
// tools assert the refusal of THIS interface), and the release it belongs
|
|
51
|
+
// to must state the change. The engine + measurement first, the wiring
|
|
52
|
+
// with the pins it rewrites — this is the same order every pulse in this
|
|
53
|
+
// repo has worked in.
|
|
54
|
+
//
|
|
55
|
+
// Byte-exactness note: decoded integers come back as Number (<= 4 bytes)
|
|
56
|
+
// or BigInt (8 bytes), exactly like src/codec.mjs's own convention, and
|
|
57
|
+
// `encodeRawLe` accepts Number, BigInt or decimal-string inputs.
|
|
58
|
+
|
|
59
|
+
class RawLeError extends Error {
|
|
60
|
+
constructor(message, path) {
|
|
61
|
+
super(path ? `${message} (${path})` : message);
|
|
62
|
+
this.name = 'RawLeError';
|
|
63
|
+
this.path = path ?? null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const PRIMITIVE_SIZES = { u8: 1, u16: 2, u32: 4, u64: 8, i64: 8 };
|
|
68
|
+
const SIGNED = new Set(['i64']);
|
|
69
|
+
|
|
70
|
+
const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
71
|
+
|
|
72
|
+
const ARRAY_SYNTAX = /^([A-Za-z_][A-Za-z0-9_]*)\[([A-Za-z_][A-Za-z0-9_]*)\]$/;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Is this IDL written in the dialect THIS engine speaks? Returns null when
|
|
76
|
+
* the whole file is inside the grammar above, or a named reason when any
|
|
77
|
+
* part of it is not. Never throws: this is the gate that decides whether
|
|
78
|
+
* the engine may even be asked.
|
|
79
|
+
*/
|
|
80
|
+
export function rawLeDialectReason(idl) {
|
|
81
|
+
if (!isPlainObject(idl)) return 'not an IDL object';
|
|
82
|
+
if (typeof idl.encoding !== 'string' || !/^little-endian\b/.test(idl.encoding)) {
|
|
83
|
+
return 'no little-endian encoding declaration — this engine speaks only the dialect the file itself declares';
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(idl.instructions) || idl.instructions.length === 0) {
|
|
86
|
+
return 'no instructions array';
|
|
87
|
+
}
|
|
88
|
+
if (idl.types !== undefined && !isPlainObject(idl.types)) {
|
|
89
|
+
return 'types must be a dict keyed by name in this dialect';
|
|
90
|
+
}
|
|
91
|
+
const typeNames = new Set(Object.keys(idl.types ?? {}));
|
|
92
|
+
|
|
93
|
+
const fieldsReason = (fields, where) => {
|
|
94
|
+
if (!Array.isArray(fields)) return `${where}: field list is not an array`;
|
|
95
|
+
let previousNames = new Set();
|
|
96
|
+
for (const [index, field] of fields.entries()) {
|
|
97
|
+
if (!isPlainObject(field) || typeof field.name !== 'string') {
|
|
98
|
+
return `${where}: field ${index} is not a named field`;
|
|
99
|
+
}
|
|
100
|
+
const reason = fieldReason(field, previousNames, typeNames, `${where}.${field.name}`);
|
|
101
|
+
if (reason) return reason;
|
|
102
|
+
if (field.optional !== undefined && index !== fields.length - 1) {
|
|
103
|
+
return `${where}.${field.name}: an optional field must be the last field — a hole in the middle cannot be told apart from garbage`;
|
|
104
|
+
}
|
|
105
|
+
previousNames.add(field.name);
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
for (const [name, type] of Object.entries(idl.types ?? {})) {
|
|
111
|
+
if (!isPlainObject(type)) return `types.${name}: not a field-list object`;
|
|
112
|
+
const reason = fieldsReason(type.fields, `types.${name}`);
|
|
113
|
+
if (reason) return reason;
|
|
114
|
+
// A numeric `size` is a fixed-size claim and must equal the field sum
|
|
115
|
+
// (measured: Hop 5 = 5x u8, Base 24 = 3x u64). A string `size` like
|
|
116
|
+
// "2 + n_hops" declares a VARIABLE size — the file says so itself, and
|
|
117
|
+
// there is nothing to check against a fixed sum.
|
|
118
|
+
if (typeof type.size === 'number') {
|
|
119
|
+
const fixed = fixedSizeOf(type.fields, typeNames);
|
|
120
|
+
if (fixed === null) {
|
|
121
|
+
return `types.${name}: declares a fixed size but its fields are not all fixed (counter arrays need a runtime count)`;
|
|
122
|
+
}
|
|
123
|
+
if (fixed !== type.size) {
|
|
124
|
+
return `types.${name}: declared size ${type.size} does not equal the field sum ${fixed}`;
|
|
125
|
+
}
|
|
126
|
+
} else if (type.size !== undefined && typeof type.size !== 'string') {
|
|
127
|
+
return `types.${name}: malformed size declaration ${JSON.stringify(type.size)}`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const tags = new Map();
|
|
132
|
+
for (const instruction of idl.instructions) {
|
|
133
|
+
if (!isPlainObject(instruction)) return 'instruction entries must be objects';
|
|
134
|
+
if (!Number.isInteger(instruction.tag) || instruction.tag < 0 || instruction.tag > 255) {
|
|
135
|
+
return `instruction ${JSON.stringify(instruction.name ?? null)}: no u8 tag — tag dispatch is the whole of this dialect`;
|
|
136
|
+
}
|
|
137
|
+
if (tags.has(instruction.tag)) {
|
|
138
|
+
return `duplicate instruction tag ${instruction.tag} (${tags.get(instruction.tag)} / ${instruction.name})`;
|
|
139
|
+
}
|
|
140
|
+
// Instructions with a tag and NO data list exist in the measured file
|
|
141
|
+
// (diabloarb `route` tag 0, `withdraw` tag 3 — owner-only stubs with
|
|
142
|
+
// empty data). The wire face of all 102 sampled payloads is tag 01,
|
|
143
|
+
// so a dispatch on such a tag is a refusal at DECODE time; the
|
|
144
|
+
// grammar itself stays decidable here.
|
|
145
|
+
if (instruction.data !== undefined && !Array.isArray(instruction.data)) {
|
|
146
|
+
return `instruction ${JSON.stringify(instruction.name)}: data must be a field list`;
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(instruction.data)) {
|
|
149
|
+
if (instruction.data.length === 0) {
|
|
150
|
+
return `instruction ${JSON.stringify(instruction.name)}: empty data list`;
|
|
151
|
+
}
|
|
152
|
+
const first = instruction.data[0];
|
|
153
|
+
if (!isPlainObject(first) || first.name !== 'tag' || first.type !== 'u8' || first.value !== instruction.tag) {
|
|
154
|
+
return `instruction ${JSON.stringify(instruction.name)}: data must start with { name: 'tag', type: 'u8', value: tag }`;
|
|
155
|
+
}
|
|
156
|
+
const reason = fieldsReason(instruction.data, `instruction ${instruction.name}`);
|
|
157
|
+
if (reason) return reason;
|
|
158
|
+
}
|
|
159
|
+
tags.set(instruction.tag, instruction.name);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function fieldReason(field, previousNames, typeNames, path) {
|
|
165
|
+
if (field.optional !== undefined && typeof field.optional !== 'string') {
|
|
166
|
+
return `${path}: optional must state its condition as prose`;
|
|
167
|
+
}
|
|
168
|
+
const type = field.type;
|
|
169
|
+
if (typeof type !== 'string') return `${path}: type must be a string in this dialect`;
|
|
170
|
+
if (PRIMITIVE_SIZES[type]) return null;
|
|
171
|
+
if (typeNames.has(type)) return null;
|
|
172
|
+
const match = ARRAY_SYNTAX.exec(type);
|
|
173
|
+
if (!match) return `${path}: unsupported type ${JSON.stringify(type.slice(0, 60))} — outside the declared grammar (u8/u16/u32/u64/i64, named types, Type[counter])`;
|
|
174
|
+
const [, element, counter] = match;
|
|
175
|
+
if (!(PRIMITIVE_SIZES[element] || typeNames.has(element))) {
|
|
176
|
+
return `${path}: array element ${JSON.stringify(element)} is not a supported type`;
|
|
177
|
+
}
|
|
178
|
+
if (!previousNames.has(counter)) {
|
|
179
|
+
return `${path}: counter ${JSON.stringify(counter)} is not a preceding field of the same list`;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The fixed byte size of a field list when all of it is fixed, else null. */
|
|
185
|
+
function fixedSizeOf(fields, typeNames) {
|
|
186
|
+
let total = 0;
|
|
187
|
+
for (const field of fields) {
|
|
188
|
+
if (field.optional !== undefined) return null; // conditionally present is not fixed
|
|
189
|
+
const size = typeFixedSize(field.type, typeNames);
|
|
190
|
+
if (size === null) return null;
|
|
191
|
+
total += size;
|
|
192
|
+
}
|
|
193
|
+
return total;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function typeFixedSize(type, typeNames) {
|
|
197
|
+
if (typeof type !== 'string') return null;
|
|
198
|
+
if (PRIMITIVE_SIZES[type]) return PRIMITIVE_SIZES[type];
|
|
199
|
+
if (!typeNames.has(type)) return null;
|
|
200
|
+
return null; // named types are resolved with their field list elsewhere
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// --- decode -----------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
class Reader {
|
|
206
|
+
constructor(bytes) {
|
|
207
|
+
this.bytes = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer ?? bytes, bytes.byteOffset ?? 0, bytes.byteLength ?? bytes.length);
|
|
208
|
+
this.offset = 0;
|
|
209
|
+
}
|
|
210
|
+
take(count, path) {
|
|
211
|
+
if (!Number.isSafeInteger(count) || count < 0 || this.offset + count > this.bytes.length) {
|
|
212
|
+
throw new RawLeError(
|
|
213
|
+
`truncated input: need ${count} byte(s) at offset ${this.offset}, only ${this.bytes.length - this.offset} left`, path);
|
|
214
|
+
}
|
|
215
|
+
const slice = this.bytes.subarray(this.offset, this.offset + count);
|
|
216
|
+
this.offset += count;
|
|
217
|
+
return slice;
|
|
218
|
+
}
|
|
219
|
+
get remaining() { return this.bytes.length - this.offset; }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const readInt = (slice, signed) => {
|
|
223
|
+
let value = 0n;
|
|
224
|
+
for (let index = slice.length - 1; index >= 0; index--) value = (value << 8n) | BigInt(slice[index]);
|
|
225
|
+
if (signed && value >= (1n << BigInt(slice.length * 8 - 1))) value -= 1n << BigInt(slice.length * 8);
|
|
226
|
+
return slice.length <= 4 ? Number(value) : value;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Decode one instruction payload with its declared raw-LE field list.
|
|
231
|
+
* `trailing: 'error'` (default 'report') refuses any byte the layout did
|
|
232
|
+
* not read — the exactness the measurement pins per payload.
|
|
233
|
+
*/
|
|
234
|
+
export function decodeRawLe(idl, data) {
|
|
235
|
+
const reason = rawLeDialectReason(idl);
|
|
236
|
+
if (reason) throw new RawLeError(`not a raw-LE dialect this engine speaks: ${reason}`, null);
|
|
237
|
+
const bytes = data instanceof Uint8Array ? data : (() => { throw new RawLeError('instruction data must be a Uint8Array/Buffer', null); })();
|
|
238
|
+
if (bytes.length === 0) throw new RawLeError('empty payload: no tag byte to dispatch on', null);
|
|
239
|
+
const instruction = idl.instructions.find((candidate) => candidate.tag === bytes[0]);
|
|
240
|
+
if (!instruction) {
|
|
241
|
+
const known = idl.instructions.map((candidate) => candidate.tag);
|
|
242
|
+
throw new RawLeError(`unknown instruction tag 0x${bytes[0].toString(16).padStart(2, '0')} (${known.length} known: ${known.join(', ')})`, null);
|
|
243
|
+
}
|
|
244
|
+
if (!Array.isArray(instruction.data)) {
|
|
245
|
+
throw new RawLeError(`instruction ${JSON.stringify(instruction.name)} declares no field list — the interface publishes no layout for this tag`, instruction.name);
|
|
246
|
+
}
|
|
247
|
+
const types = idl.types ?? {};
|
|
248
|
+
const reader = new Reader(bytes);
|
|
249
|
+
reader.take(1, `${instruction.name}.tag`); // the tag dispatch byte, re-read below as a field
|
|
250
|
+
reader.offset = 0;
|
|
251
|
+
const args = {};
|
|
252
|
+
const absent = [];
|
|
253
|
+
const fieldValues = new Map();
|
|
254
|
+
for (const field of instruction.data) {
|
|
255
|
+
const fieldPath = `${instruction.name}.${field.name}`;
|
|
256
|
+
const valueOrAbsent = readField(field, reader, types, fieldValues, fieldPath);
|
|
257
|
+
if (valueOrAbsent === ABSENT) { absent.push(field.name); continue; }
|
|
258
|
+
args[field.name] = valueOrAbsent;
|
|
259
|
+
fieldValues.set(field.name, valueOrAbsent);
|
|
260
|
+
}
|
|
261
|
+
const trailingCount = reader.remaining;
|
|
262
|
+
if (trailingCount > 0) {
|
|
263
|
+
throw new RawLeError(
|
|
264
|
+
`trailing bytes after ${instruction.name}: consumed ${reader.offset} of ${bytes.length}`, instruction.name);
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
kind: 'instruction',
|
|
268
|
+
instruction: instruction.name,
|
|
269
|
+
tag: bytes[0],
|
|
270
|
+
args,
|
|
271
|
+
absent,
|
|
272
|
+
consumed: reader.offset,
|
|
273
|
+
total: bytes.length,
|
|
274
|
+
trailing: { count: trailingCount, hex: '' },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const ABSENT = Symbol('absent');
|
|
279
|
+
|
|
280
|
+
function readField(field, reader, types, fieldValues, path) {
|
|
281
|
+
// Optional fields are decided by the bytes: present exactly when the full
|
|
282
|
+
// size fits the remainder to zero and the field is last.
|
|
283
|
+
if (field.optional !== undefined) {
|
|
284
|
+
const size = declaredFieldByteSize(field.type, types, fieldValues, path, reader.remaining);
|
|
285
|
+
if (size === null || size !== reader.remaining) return ABSENT;
|
|
286
|
+
}
|
|
287
|
+
return readValue(field.type, reader, types, fieldValues, path);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function resolveType(type, types, path) {
|
|
291
|
+
if (PRIMITIVE_SIZES[type]) return { kind: 'primitive', name: type };
|
|
292
|
+
if (Object.prototype.hasOwnProperty.call(types, type)) return { kind: 'named', name: type };
|
|
293
|
+
const match = ARRAY_SYNTAX.exec(type);
|
|
294
|
+
if (match) return { kind: 'array', element: match[1], counter: match[2] };
|
|
295
|
+
throw new RawLeError(`unsupported type ${JSON.stringify(type)}`, path);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function readValue(type, reader, types, fieldValues, path) {
|
|
299
|
+
const resolved = resolveType(type, types, path);
|
|
300
|
+
if (resolved.kind === 'primitive') {
|
|
301
|
+
const slice = reader.take(PRIMITIVE_SIZES[resolved.name], path);
|
|
302
|
+
return readInt(slice, SIGNED.has(resolved.name));
|
|
303
|
+
}
|
|
304
|
+
if (resolved.kind === 'named') {
|
|
305
|
+
return readFields(types[resolved.name].fields, reader, types, path, {});
|
|
306
|
+
}
|
|
307
|
+
const countRaw = fieldValues.get(resolved.counter);
|
|
308
|
+
const count = typeof countRaw === 'bigint' ? Number(countRaw) : countRaw;
|
|
309
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
310
|
+
throw new RawLeError(`counter ${JSON.stringify(resolved.counter)} is not a usable count`, path);
|
|
311
|
+
}
|
|
312
|
+
const out = [];
|
|
313
|
+
for (let index = 0; index < count; index++) {
|
|
314
|
+
out.push(readValue(resolved.element, reader, types, fieldValues, `${path}[${index}]`));
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function readFields(fields, reader, types, path, args) {
|
|
320
|
+
const fieldValues = new Map();
|
|
321
|
+
const absent = [];
|
|
322
|
+
for (const field of fields) {
|
|
323
|
+
const fieldPath = `${path}.${field.name}`;
|
|
324
|
+
let value;
|
|
325
|
+
if (field.optional !== undefined) {
|
|
326
|
+
const size = declaredFieldByteSize(field.type, types, fieldValues, fieldPath, reader.remaining);
|
|
327
|
+
value = size !== null && size === reader.remaining
|
|
328
|
+
? readValue(field.type, reader, types, fieldValues, fieldPath)
|
|
329
|
+
: ABSENT;
|
|
330
|
+
} else {
|
|
331
|
+
value = readValue(field.type, reader, types, fieldValues, fieldPath);
|
|
332
|
+
}
|
|
333
|
+
if (value === ABSENT) { absent.push(field.name); continue; }
|
|
334
|
+
args[field.name] = value;
|
|
335
|
+
fieldValues.set(field.name, value);
|
|
336
|
+
}
|
|
337
|
+
args.absent = absent;
|
|
338
|
+
return args;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// --- encode -----------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
const toBigInt = (value, path) => {
|
|
344
|
+
if (typeof value === 'bigint') return value;
|
|
345
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) return BigInt(value);
|
|
346
|
+
if (typeof value === 'string' && /^-?\d+$/.test(value)) return BigInt(value);
|
|
347
|
+
throw new RawLeError(`expected an integer, got ${typeof value}`, path);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const writeInt = (size, signed, value, path) => {
|
|
351
|
+
const bits = BigInt(size * 8);
|
|
352
|
+
if (signed) {
|
|
353
|
+
if (value < -(1n << (bits - 1n)) || value >= 1n << (bits - 1n)) {
|
|
354
|
+
throw new RawLeError(`${value} out of range for ${size}-byte signed integer`, path);
|
|
355
|
+
}
|
|
356
|
+
if (value < 0n) value += 1n << bits;
|
|
357
|
+
} else if (value < 0n || value >= 1n << bits) {
|
|
358
|
+
throw new RawLeError(`${value} out of range for ${size}-byte unsigned integer`, path);
|
|
359
|
+
}
|
|
360
|
+
const out = Buffer.alloc(size);
|
|
361
|
+
for (let index = 0; index < size; index++) { out[index] = Number(value & 0xffn); value >>= 8n; }
|
|
362
|
+
return out;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/** Encode one instruction payload back to its exact wire bytes.
|
|
366
|
+
* `absent` names optional fields to omit — same call shape as the codec's
|
|
367
|
+
* own encodeInstructionData ({ absent: [...] }), and args.absent works too. */
|
|
368
|
+
export function encodeRawLe(idl, name, args, { absent } = {}) {
|
|
369
|
+
const reason = rawLeDialectReason(idl);
|
|
370
|
+
if (reason) throw new RawLeError(`not a raw-LE dialect this engine speaks: ${reason}`, null);
|
|
371
|
+
const instruction = idl.instructions.find((candidate) => candidate.name === name);
|
|
372
|
+
if (!instruction) throw new RawLeError(`unknown instruction ${JSON.stringify(name)}`, null);
|
|
373
|
+
const types = idl.types ?? {};
|
|
374
|
+
const parts = [];
|
|
375
|
+
const fieldValues = new Map();
|
|
376
|
+
const absentSet = new Set(Array.isArray(absent) ? absent : (Array.isArray(args?.absent) ? args.absent : []));
|
|
377
|
+
for (const field of instruction.data) {
|
|
378
|
+
const fieldPath = `${instruction.name}.${field.name}`;
|
|
379
|
+
if (absentSet.has(field.name)) {
|
|
380
|
+
if (field.optional === undefined) {
|
|
381
|
+
throw new RawLeError(`field ${JSON.stringify(field.name)} is not optional — it cannot be absent`, fieldPath);
|
|
382
|
+
}
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const value = args?.[field.name];
|
|
386
|
+
if (value === undefined) throw new RawLeError(`missing argument ${JSON.stringify(field.name)}`, fieldPath);
|
|
387
|
+
if (field.type === 'u8' && field.name === 'tag') {
|
|
388
|
+
const tag = toBigInt(value, fieldPath);
|
|
389
|
+
if (tag !== BigInt(instruction.tag)) throw new RawLeError(`tag value ${value} disagrees with the instruction tag ${instruction.tag}`, fieldPath);
|
|
390
|
+
parts.push(Buffer.from([instruction.tag]));
|
|
391
|
+
fieldValues.set(field.name, Number(tag));
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
parts.push(encodeValue(field.type, value, types, fieldValues, fieldPath));
|
|
395
|
+
fieldValues.set(field.name, value);
|
|
396
|
+
}
|
|
397
|
+
return Buffer.concat(parts);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function encodeValue(type, value, types, fieldValues, path) {
|
|
401
|
+
const resolved = resolveType(type, types, path);
|
|
402
|
+
if (resolved.kind === 'primitive') {
|
|
403
|
+
return writeInt(PRIMITIVE_SIZES[resolved.name], SIGNED.has(resolved.name), toBigInt(value, path), path);
|
|
404
|
+
}
|
|
405
|
+
if (resolved.kind === 'named') {
|
|
406
|
+
if (!isPlainObject(value)) throw new RawLeError(`expected an object for named type ${JSON.stringify(resolved.name)}`, path);
|
|
407
|
+
const localValues = new Map();
|
|
408
|
+
const parts = [];
|
|
409
|
+
const absentSet = new Set(Array.isArray(value.absent) ? value.absent : []);
|
|
410
|
+
for (const field of types[resolved.name].fields) {
|
|
411
|
+
const fieldPath = `${path}.${field.name}`;
|
|
412
|
+
if (absentSet.has(field.name)) {
|
|
413
|
+
if (field.optional === undefined) throw new RawLeError(`field ${JSON.stringify(field.name)} is not optional`, fieldPath);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (value[field.name] === undefined) throw new RawLeError(`missing field ${JSON.stringify(field.name)} of ${resolved.name}`, fieldPath);
|
|
417
|
+
parts.push(encodeValue(field.type, value[field.name], types, localValues, fieldPath));
|
|
418
|
+
localValues.set(field.name, value[field.name]);
|
|
419
|
+
}
|
|
420
|
+
return Buffer.concat(parts);
|
|
421
|
+
}
|
|
422
|
+
if (!Array.isArray(value)) throw new RawLeError(`expected an array for ${JSON.stringify(type)}`, path);
|
|
423
|
+
const declaredCount = fieldValues.get(resolved.counter);
|
|
424
|
+
const count = typeof declaredCount === 'bigint' ? Number(declaredCount) : declaredCount;
|
|
425
|
+
if (value.length !== count) {
|
|
426
|
+
throw new RawLeError(`array of ${value.length} item(s) contradicts counter ${JSON.stringify(resolved.counter)}=${count}`, path);
|
|
427
|
+
}
|
|
428
|
+
return Buffer.concat(value.map((item, index) => encodeValue(resolved.element, item, types, fieldValues, `${path}[${index}]`)));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// The byte size a field occupies when it is fixed-size; null when the size
|
|
432
|
+
// depends on runtime counts (arrays, and named types containing them) —
|
|
433
|
+
// the optional-field presence check only fires for fixed-size fields, so
|
|
434
|
+
// 'u8[n_hops]'-style shapes never pretend to be optional-sized.
|
|
435
|
+
function declaredFieldByteSize(type, types, fieldValues, path, max) {
|
|
436
|
+
const resolved = resolveType(type, types, path);
|
|
437
|
+
if (resolved.kind === 'primitive') return PRIMITIVE_SIZES[resolved.name];
|
|
438
|
+
if (resolved.kind === 'named') {
|
|
439
|
+
let total = 0;
|
|
440
|
+
for (const field of types[resolved.name].fields) {
|
|
441
|
+
const size = declaredFieldByteSize(field.type, types, fieldValues, `${path}.${field.name}`, max);
|
|
442
|
+
if (size === null) return null;
|
|
443
|
+
total += size;
|
|
444
|
+
}
|
|
445
|
+
return total;
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export { RawLeError };
|