sdk-core: adopt wire-sysio table_id namespace isolation#5
Conversation
Wire-sysio PR Wire-Network/wire-sysio#288 widened table_def.name from sysio::name (uint64) to a free-form std::string and appended two new fields — table_id (uint16, DJB2 hash of the table name % 65536) and secondary_indexes (vector<index_def>) — for KV-table per-table namespace isolation. The wire format break means any sdk-core caller that loads a contract ABI from on-chain via get_raw_abi (or any other binary-encoded source) parses misaligned starting at the first table field — name reads the wrong bytes, every subsequent field is shifted, and ricardian_clauses ends up reading garbage. PR Wire-Network/wire-sysio#290 separately changed the get_table_rows HTTP response shape for KV-backed tables. Each row used to be the decoded struct directly (or {data, payer} when show_payer was set); the unified endpoint now returns {key, value, payer?} per row. The ChainAPI wrapper used to destructure {data, payer} on show_payer and then map rows through Serializer.decode for typed callers — both paths break against the new shape. This commit takes a hard break on the binary format (no fallback to the legacy EOSIO 8-byte name encoding) and a backward-compatible unwrap on the HTTP response shape. packages/sdk-core/src/chain/Abi.ts: - new ABI.Index interface (name, key_type, table_id) mirroring sysio::chain::index_def in wire-sysio's libraries/chain/include/sysio/chain/abi_def.hpp. - ABI.Table gains optional table_id (uint16) and secondary_indexes (Index[]) fields. Optional so hand-built test fixtures and ABIs constructed programmatically don't need to specify them; the encoder defaults to 0 / [] when omitted. - fromABI() reads name as a length-prefixed string instead of an 8-byte sysio::name uint64, then consumes table_id (uint16 LE) and secondary_indexes (vector<index_def>) after the existing type field. Also reads a trailing protobuf_types extension after enums so that any subsequent extension appended to abi_def parses cleanly to EOF. - toABI() mirrors the parser: writes table_def.name via encoder.writeString(String(table.name)) (the String() coercion keeps NameType-typed callers compiling), then table_id and secondary_indexes, and finally the empty protobuf_types string. packages/sdk-core/src/api/v1/Chain.ts: - get_table_rows() now detects the unified KV row shape — each row has both `key` and `value` keys — and unwraps to the inner value before downstream processing. The old EOSIO shapes (decoded struct directly, or {data, payer} on show_payer) are left untouched, so sdk-core remains usable against EOSIO chains. - When show_payer is set on a KV row, payer is captured into ram_payers in the same way as the legacy {data, payer} path. Missing payer fields coerce to an empty Name rather than throwing. Tests (15 new cases, 304/304 sdk-core suite green): packages/sdk-core/tests/chain/Abi.test.ts (8 cases) - Round-trips a table with table_id + empty secondary_indexes - Round-trips a long table name (>12 chars) — the whole reason name was widened - Round-trips secondary_indexes with checksum256 key_type (the case the wire-cdt #49 abigen fix unblocked end-to-end) - table_id 0 and 65535 boundary values - Missing table_id defaults to 0 on encode + decode - protobuf_types extension is consumed without affecting enums - Encoder always emits the empty protobuf_types trailer - Multi-table ABI end-to-end round-trip (structs + actions + secondary_indexes) packages/sdk-core/tests/api/v1/Chain.test.ts (7 cases) - Unwraps the new {key, value} shape into plain rows - Unwraps the new shape with show_payer + captures ram_payers - Preserves legacy plain-row shape from EOSIO chains - Preserves legacy {data, payer} show_payer shape from EOSIO - Empty rows array works for both shapes - Missing payer in new shape coerces to empty name (no throw) - Uses an in-memory MockProvider so no network access required Out of scope: - packages/sdk-core/src/resources/{Ram,Rex,Powerup}.ts call get_table_rows for tables (rammarket, rex_pool, powup_state) that do not exist in wire-sysio and have not for some time. These resources were already dead on Wire chains regardless of any of the in-flight wire-sysio PRs and remain dead after this commit. - packages/sdk-core/src/types/SystemContractTypes.ts is auto- generated and would benefit from a regen pass after sysio.token's migration to kv::scoped_table merges (Wire-Network/wire-sysio#291), but the field schemas of the structs it generates do not depend on the binary table_def layout. Companion PRs: - Wire-Network/wire-sysio#288 — chain-side table_id namespace isolation - Wire-Network/wire-sysio#290 — unified get_table_rows API - Wire-Network/wire-sysio#291 — sysio.token migrated to kv::scoped_table - Wire-Network/wire-cdt#49 — kv::scoped_table + abigen secondary_indexes - Wire-Network/wirejs-native#4 — TS schema mirror for abi_def - Wire-Network/abieos#12 — abieos C API + binary format update - Wire-Network/node-abieos#8 — bumps abieos + drops the lossy uint64 round-trip - Wire-Network/hyperion-history-api#9 — KV delta handler + AbiDefinitions
Mirrors Wire-Network/wire-libraries-ts#5 (packages/sdk-core). Binary ABI (hard break): table_def.name is now a length-prefixed string (was an 8-byte sysio::name uint64). table_id (uint16 LE, DJB2 hash of the table name % 65536) and secondary_indexes (vector<index_def>) are appended to each table entry. A protobuf_types trailing extension field is also read/written for forward compatibility. The ABI.Table interface gains optional table_id and secondary_indexes fields and a new ABI.Index interface. This mirrors wire-sysio Wire-Network/wire-sysio#288. get_table_rows response shape (back-compat): KV-backed tables on wire-sysio Wire-Network/wire-sysio#290+ return each row as {key, value, payer?} instead of the decoded struct directly. ChainAPI detects this shape and unwraps to expose just the value (and captures payer into ram_payers when show_payer is set), preserving the existing API contract for callers. EOSIO chains emitting the legacy flat or {data,payer} shapes are unaffected. Tests: test/abi.ts covers table_id round-trip, long names, secondary indexes, boundary values (0/65535), missing-field defaults, the protobuf_types trailer, and a multi-table end-to-end encode/decode. test/chain-api.ts covers the new KV shape unwrap, show_payer capture, and backward compatibility with both legacy row shapes.
dtaghavi
left a comment
There was a problem hiding this comment.
Code Review
Overall this is a clean, well-scoped PR with clear motivation and good test coverage. The summary and companion-PR links make the context easy to follow. A few things worth discussing:
Chain.ts — KV shape detection
Potential false positive on the shape heuristic
const isWireKvShape =
Array.isArray(rows) &&
rows.length > 0 &&
typeof rows[0] === "object" &&
rows[0] !== null &&
"key" in rows[0] &&
"value" in rows[0]Any legacy EOSIO table whose row struct happens to have fields named key and value will be incorrectly treated as the new KV shape. eosio.token's stat table ({supply, max_supply, issuer}) is safe, but contracts with generic key/value column names are not. A stronger discriminant—e.g. checking that rows[0].key is itself an object (composite key), or verifying the absence of other decoded fields—would tighten this. Even a comment acknowledging the known collision risk would help future maintainers.
show_payer without KV shape still throws on missing payer
The new KV path uses row.payer ?? "" (safe), but the legacy path still does:
rows = rows.map(({ data, payer }) => {
ram_payers!.push(Name.from(payer)) // throws if payer is undefined
return data
})If a legacy node omits payer for a show_payer request, the behaviour is now asymmetric between the two paths. Low-risk in practice, but worth making consistent.
Abi.ts — Binary parser
canRead() only skips one trailing extension
if (decoder.canRead()) {
decoder.readString() // discard
}This consumes exactly one trailing field. If a future abi_def appends a second extension after protobuf_types, the parser will leave bytes on the decoder and either mis-parse subsequent fields or silently succeed with a garbage ABI. while (decoder.canRead()) { decoder.readString() } would be strictly more future-proof at essentially no cost.
table_id overflow is silently truncated
The encoder masks to uint16:
encoder.writeByte(tid & 0xff)
encoder.writeByte((tid >> 8) & 0xff)A caller passing table_id: 70000 gets silently truncated to 70000 % 65536 = 4464. A runtime guard (if (tid > 0xffff) throw ...) or at minimum a comment would prevent subtle bugs in hand-built ABIs.
Manual byte assembly instead of a helper
tidLo | (tidHi << 8) appears three times (table + each index). If ABIDecoder grows a readUint16LE() helper later, this pattern would need updating in multiple places. Not a blocker, just worth noting.
Tests
encoder always emits protobuf_types test is brittle
expect(bytes[bytes.length - 1]).toBe(0x00)This relies on an empty ABI happening to produce a trailing 0x00 byte, which only holds because the varuint encoding of length-0 is 0x00. If any other field is added after protobuf_types in the future this test will silently pass for the wrong reason. Round-tripping via decodeAbi(encodeAbi(abi)) and asserting on the decoded enums length (already done elsewhere) would be more robust.
No negative test for table_id > 65535
Given the silent-truncation issue above, a test asserting the truncation behaviour (or the error, if a guard is added) would pin the contract and prevent a future regression.
isWireKvShape false-positive case is not tested
A test with a legacy table having both key and value fields (e.g. {key: "primary", value: 42, owner: "alice"}) would document the current behaviour and catch any future change to the detection heuristic.
Minor / nits
String(table.name)works but will stringify aNameobject as its canonical string representation — worth a one-line comment confirming that's the intended coercion path, since it's the same valueName.toString()returns.- The
ABI.Index.table_idfield is required, whileABI.Table.table_idis optional. The asymmetry is defensible (secondary indexes always have one on-chain) but a JSDoc note onIndex.table_idwould help callers building ABI objects programmatically understand why they can't omit it. out of scopesection in the PR body is a nice touch — appreciated.
Summary
The binary-format break is clearly necessary and well-documented. The KV shape unwrap is backward-compatible and the test matrix is thorough. The main things I'd want addressed before merging are:
- Harden or document the KV shape heuristic against the false-positive case.
- Either guard against
table_id > 65535or document the truncation. - Consider
while (decoder.canRead())for the protobuf_types trailer.
Everything else is minor. Tests are solid and the companion-PR chain is clearly laid out.
Align with the approved sdk-core#10 follow-up so the two mirrored packages stay consistent. - Tighten isWireKvShape: require `key` to be a non-null object (wire- sysio KV keys are always composite scope+primary_key) so user tables with scalar `key`/`value` fields are not misinterpreted. - Report missing payer as `undefined` on both the KV and legacy show_payer paths; widen GetTableRowsResponse.ram_payers to (Name | undefined)[] so the absent-payer case is explicit. - Widen ABI.Table.name from NameType to string (drop the String() coercion); names > 12 chars and arbitrary characters are valid. - Guard table_id / index table_id as uint16 on encode via ABI.assertUint16 so out-of-range values throw rather than silently truncating. - Drain all remaining string-typed trailing extensions (while-loop) so a later-appended extension doesn't break decoding. - Align optionality: ABI.Index.table_id is optional (matches ABI.Table.table_id), encoder falls back to 0. - Trim verbose inline wire-sysio PR references from comments; keep the "why" (format changed). Tests: - table_id and index table_id out-of-range throw. - decoder drains multiple trailing string-typed extensions. - golden-bytes fixture pinning the table_def binary layout. - user table with scalar key+value is not unwrapped. - missing payer in KV shape is reported as undefined, not "". All 309 sdk-core tests pass.
dtaghavi
left a comment
There was a problem hiding this comment.
Follow-up Review (current diff)
The previous review was written against an earlier iteration. Several of those issues have since been addressed — the typeof rows[0].key === \"object\" discriminant was added, the drain loop was widened to while (decoder.canRead()), assertUint16 was introduced, and the legacy show_payer null-safety was fixed. This pass covers what remains.
Chain.ts — KV shape detection
Heuristic can still misidentify user tables with an object key field
The updated check correctly rejects scalar key values, but a user-defined table like {key: {name: \"alice\"}, value: 42} (where key is an embedded struct) still triggers the KV path and would be silently unwrapped. The existing test covers scalar key/value but not the object-key false-positive case. At minimum, a comment noting this residual risk (or a doc test demonstrating the known gap) would help future maintainers.
Abi.ts — Binary parser
while (decoder.canRead()) drain loop is fragile against non-string extensions
The comment already calls this out: "Non-string extensions added in the future will need explicit handling here." The risk is that a future abi_def extension using a non-string wire type (e.g. a varuint count followed by entries) would cause the drain loop to misread the length prefix as a string length and silently produce a corrupt ABI. A version string comparison (e.g. version >= \"sysio::abi/1.3\" → drain) would scope the drain to known-good versions and force an explicit update when the wire format is extended again.
Duplicated two-byte uint16 LE reads
const tidLo = decoder.readByte()
const tidHi = decoder.readByte()
const table_id = tidLo | (tidHi << 8)This pattern appears three times (once for the table, once per secondary index). If ABIDecoder ever gains a readUint16LE() helper this will need three updates. A local inline helper or a note to consolidate if the decoder is extended would be low-cost.
Types.ts — Breaking type change
ram_payers?: (Name | undefined)[] is a public API break
The JSDoc comment on GetTableRowsResponse.ram_payers is clear, but any downstream consumer iterating ram_payers with Name calls (e.g. p.toString()) will silently get a runtime error once they hit an undefined entry from a KV row with a missing payer. This is expected given the wire-sysio shape, but worth a changelog entry or a migration note in the PR body so integrators know to audit their payer consumers.
Tests
No test for the object-key false-positive case
The does not unwrap user table that happens to have scalar key+value fields test covers {key: string, value: number}. A companion case with {key: {scope: \"alice\"}, value: 42} (object key, scalar value) would document whether the current heuristic is conservative enough for that shape or not.
encoder always emits protobuf_types test checks a single trailing byte
expect(bytes[bytes.length - 1]).toBe(0x00)This is correct today but will silently pass for the wrong reason once any other zero-valued byte lands at the end. The golden-bytes test covers this more robustly — it may be worth dropping the single-byte assertion or replacing it with a decode round-trip + enums.length check.
Nits
ABI.Index.table_idis typednumber | undefined(optional?) even though the chain always provides it. Marking it required (or at minimum adding a note that the encoder defaults to0) would reduce ambiguity for callers buildingIndexobjects programmatically.- The inline comment
// Binary order matches sysio::chain::table_def.infromABIis useful — worth mirroring the same note intoABIso the encoder is equally self-documenting.
Summary
The main items still worth addressing:
- Object-key false-positive in
isWireKvShape— add a test (and optionally a comment) documenting the residual case. while (decoder.canRead())fragility — consider scoping to a known version range or adding a forward-compat comment with a clear escalation path.ram_payersbreakage — add a migration note in the PR body or CHANGELOG for downstream integrators.
Everything else is minor. The test suite is thorough, the golden-bytes fixture is a great safeguard against silent wire-format drift, and the companion-PR chain is clearly laid out. Code is ready to merge once the above are discussed.
- Abi.ts: version-gate trailing-string drain to sysio::abi/1.x so a future non-string extension fails safely instead of being mis-parsed as a string length - Abi.ts: extract readUint16LE/writeUint16LE helpers to collapse the three duplicated table_id encode/decode sites - Abi.ts: mirror the "Binary order matches sysio::chain::table_def" comment in toABI so both sides are equally self-documenting - Abi.ts: clarify Index.table_id JSDoc - required on-chain, defaults to 0 on encode when omitted from a hand-built ABI - Chain.ts: document the residual object-key false-positive in isWireKvShape and add a pinned regression test - Abi.test.ts: replace brittle bytes[length-1] === 0x00 check with a populated-ABI round-trip; add test for the version-gated drain
dtaghavi
left a comment
There was a problem hiding this comment.
Third-pass review — all prior feedback addressed
Kevin addressed every item from the two prior review passes. This is a clean final state. A few small observations below, none of them blockers.
What landed well
readUint16LE/writeUint16LEhelpers — the duplication is gone and the call sites are readable.- Version-gated drain (
version.startsWith("sysio::abi/1.")) — exactly the right scope. The test that appends[0xff, 0xff, 0xff, 0xff, 0x01]to asysio::abi/2.0ABI is a good proof that the gate prevents mis-parse rather than just silently skipping. - The pinned false-positive test in
Chain.test.ts(does not unwrap user table whose key is itself an object) — documenting known-bad behavior with a comment pointing to a future improvement path is the right call here. Index.table_idJSDoc — the "required on-chain, optional for hand-built" distinction is now explicit.
Minor observations (not blocking)
Encoder always emits protobuf_types regardless of version
toABI unconditionally writes encoder.writeString("") at the end. For an ABI with version: "sysio::abi/2.0", the encoded bytes will include the extension even though the decoder's version gate would skip draining it. In practice there is no sysio::abi/2.0 today so this is harmless, but a comment like // sysio::abi/1.x always ends with protobuf_types co-located with the writeString("") call would make the coupling explicit if the version format ever changes.
protobuf_types test still carries the single-byte assertion
expect(bytes[bytes.length - 1]).toBe(0x00)The added round-trip check (verifying decoded.enums is preserved) significantly reduces the brittleness concern — the trailing 0x00 can now only be an empty string terminator rather than a coincidental zero. The golden-bytes test below it is still the authoritative pin. No change needed; just noting it's fine as-is.
No CHANGELOG / migration note for ram_payers type widening
GetTableRowsResponse.ram_payers changed from Name[] to (Name | undefined)[]. The JSDoc comment covers the intent clearly. If there's a CHANGELOG or BREAKING-CHANGES file in the repo it would be worth a one-liner there for integrators auditing their payer consumers — but if the project doesn't maintain one, the JSDoc is sufficient.
Verdict
All three major items from the prior reviews are resolved: the version-gated drain handles the fragility concern, readUint16LE/writeUint16LE collapsed the duplication, and the object-key false-positive is documented with a pinned regression test. Test count went from 304 to 309 and the golden-bytes fixture gives strong protection against silent wire-format drift.
Approved.
- Chain.ts: hoist inline row-shape casts to named WireKvRow type; tighten isWireKvShape to reject array-valued `key` fields - Abi.ts: version-gate the encoder's protobuf_types trailer on sysio::abi/1. to match the decoder's gate, so non-1.x ABIs round-trip byte-exact without a trailer - Abi.test.ts: update golden-bytes fixture to sysio::abi/1.2; add companion golden test for non-1.x (no trailer); rename the protobuf_types emission test to reflect version gating - Chain.test.ts: add regression test for user tables whose `key` field is an array (previously matched the heuristic, now rejected)
dtaghavi
left a comment
There was a problem hiding this comment.
Fourth-pass review — third-pass feedback fully addressed
All three items from the last review are resolved:
| Prior concern | Resolution |
|---|---|
Encoder always emits protobuf_types regardless of version |
toABI is now version-gated (this.version.startsWith("sysio::abi/1.")) — symmetric with the decoder |
Array-valued key field false-positive (typeof [] === "object") |
!Array.isArray(rows[0].key) added; new test does not unwrap user table whose key field is an array pins the behavior |
Golden bytes test used short version "v" (non-1.x) |
Now uses "sysio::abi/1.2"; a companion golden bytes test for "sysio::abi/2.0" proves the trailer is absent for non-1.x versions |
The WireKvRow type alias is a nice touch — it removes the inline any casts and documents the expected wire shape in one place.
Two minor nits (not blocking)
WireKvRow is defined mid-function, after the expression that uses it
const isWireKvShape = ... // computed first
type WireKvRow = { ... } // type alias follows
...
rows.map((row: WireKvRow) => ...) // used hereTypeScript hoists type aliases so this compiles, but readers scanning the function top-to-bottom hit the reference before the definition. Moving WireKvRow to just above isWireKvShape (or to module scope) would make the order read naturally.
decoder skips protobuf_types test relies on the implicit default version
const original = new ABI({ enums: [...] })The test comment says the encoder emits the trailer for sysio::abi/1.x, but doesn't specify a version, leaning on the default being sysio::abi/1.1. Adding version: "sysio::abi/1.2" explicitly — like the other tests in this file — would make the assumption visible and keep the test self-contained if the default ever changes.
Verdict
The encoder/decoder symmetry is now tight, the heuristic discrimination is as strong as it can reasonably be without ABI awareness, and the golden bytes fixtures give solid protection against future drift.
Approved.
- Hoist WireKvRow type alias above isWireKvShape so the function reads top-to-bottom instead of relying on TS hoisting. - Pin version: "sysio::abi/1.2" in the "decoder skips protobuf_types" test so it no longer leans on the implicit default version.
jglanz
left a comment
There was a problem hiding this comment.
Looks good, you may need to rebase
Align with the approved sdk-core#10 follow-up so the two mirrored packages stay consistent. - Tighten isWireKvShape: require `key` to be a non-null object (wire- sysio KV keys are always composite scope+primary_key) so user tables with scalar `key`/`value` fields are not misinterpreted. - Report missing payer as `undefined` on both the KV and legacy show_payer paths; widen GetTableRowsResponse.ram_payers to (Name | undefined)[] so the absent-payer case is explicit. - Widen ABI.Table.name from NameType to string (drop the String() coercion); names > 12 chars and arbitrary characters are valid. - Guard table_id / index table_id as uint16 on encode via ABI.assertUint16 so out-of-range values throw rather than silently truncating. - Drain all remaining string-typed trailing extensions (while-loop) so a later-appended extension doesn't break decoding. - Align optionality: ABI.Index.table_id is optional (matches ABI.Table.table_id), encoder falls back to 0. - Trim verbose inline wire-sysio PR references from comments; keep the "why" (format changed). Tests: - table_id and index table_id out-of-range throw. - decoder drains multiple trailing string-typed extensions. - golden-bytes fixture pinning the table_def binary layout. - user table with scalar key+value is not unwrapped. - missing payer in KV shape is reported as undefined, not "". All 309 sdk-core tests pass.
- Abi.ts: version-gate trailing-string drain to sysio::abi/1.x so a future non-string extension fails safely instead of being mis-parsed as a string length - Abi.ts: extract readUint16LE/writeUint16LE helpers to collapse the three duplicated table_id encode/decode sites - Abi.ts: mirror the "Binary order matches sysio::chain::table_def" comment in toABI so both sides are equally self-documenting - Abi.ts: clarify Index.table_id JSDoc - required on-chain, defaults to 0 on encode when omitted from a hand-built ABI - Chain.ts: document the residual object-key false-positive in isWireKvShape and add a pinned regression test - Abi.test.ts: replace brittle bytes[length-1] === 0x00 check with a populated-ABI round-trip; add test for the version-gated drain
- Chain.ts: hoist inline row-shape casts to named WireKvRow type; tighten isWireKvShape to reject array-valued `key` fields - Abi.ts: version-gate the encoder's protobuf_types trailer on sysio::abi/1. to match the decoder's gate, so non-1.x ABIs round-trip byte-exact without a trailer - Abi.test.ts: update golden-bytes fixture to sysio::abi/1.2; add companion golden test for non-1.x (no trailer); rename the protobuf_types emission test to reflect version gating - Chain.test.ts: add regression test for user tables whose `key` field is an array (previously matched the heuristic, now rejected)
Summary
Wire-sysio PR Wire-Network/wire-sysio#288 widened
table_def.namefromsysio::name(uint64) to free-formstd::stringand appendedtable_id(uint16, DJB2 hash of the table name % 65536) andsecondary_indexes(vector<index_def>) for KV-table per-table namespace isolation. The wire format break means any sdk-core caller that loads a contract ABI from on-chain viaget_raw_abiparses misaligned starting at the first table —namereads the wrong bytes, every subsequent field is shifted, andricardian_clausesends up reading garbage.PR Wire-Network/wire-sysio#290 separately changed the
get_table_rowsHTTP response shape for KV-backed tables. Each row used to be the decoded struct directly (or{data, payer}whenshow_payerwas set); the unified endpoint now returns{key, value, payer?}per row.This PR takes a hard break on the binary format (no fallback to the legacy EOSIO 8-byte name encoding) and a backward-compatible unwrap on the HTTP response shape.
Companion PRs:
get_table_rowsAPIkv::scoped_tablekv::scoped_table+ abigen secondary_indexesabi_defAbiDefinitionsHard binary-format break
ABI.fromABI()now only parses wire-sysio binary ABIs (table_def.nameas a string). Loading a binary ABI from a classical EOSIO chain viaget_raw_abiwould mis-parse — those chains still emit the legacy 8-byte name encoding. Two paths around this if needed:ABI.from(jsonString)) for cross-chain code — JSON parsing is unchangedBreaking type change:
ram_payersentries may be undefinedGetTableRowsResponse.ram_payerswidened fromName[]to(Name | undefined)[]. Wire-sysio KV rows returnpayeras an optional field; when it is absent, the wrapper now pushesundefinedso the absent-payer case is explicit rather than silently coerced intoName.from(undefined)(which previously threw at runtime).Downstream code that iterates
ram_payersand calls methods on entries directly must now guard againstundefined:Legacy EOSIO responses with fully-populated
payerfields are unaffected at runtime but will still need the guard to satisfy the compiler.Changes
packages/sdk-core/src/chain/Abi.tsABI.Indexinterface ({name, key_type, table_id}) mirroringsysio::chain::index_def.ABI.Tablegains optionaltable_id: numberandsecondary_indexes: Index[]fields. Optional so hand-built test fixtures and ABIs constructed programmatically don't need to specify them; the encoder defaults to0/[]when omitted.fromABI():nameas a length-prefixed string instead ofName.fromABI(decoder)(the 8-byte sysio::name path)table_id(uint16 LE) andsecondary_indexes(vector<index_def>) after the existingtypefieldprotobuf_typesextension afterenums; the drain loop is version-gated tosysio::abi/1.xso a future non-string extension fails safely rather than silently corrupting the decoded ABItoABI()mirrors the parser. TheTable.namefield is now typedstring(wasNameType) since the wire format carries a free-form string; the encoder writes it directly viaencoder.writeString(table.name). The trailing emptyprotobuf_typesis version-gated tosysio::abi/1.so round-tripping a non-1.x ABI stays byte-exact.readUint16LE/writeUint16LEhelpers collapse the three duplicated sites (table + 2 in the secondary-index loop) into one path.packages/sdk-core/src/api/v1/Chain.tsget_table_rows()now detects the unified KV row shape (each row has bothkeyandvaluekeys, andkeyis itself a non-null object) and unwraps to the innervaluebefore downstream processing. The legacy EOSIO shapes (decoded struct directly, or{data, payer}onshow_payer) are left untouched.Requiring
keyto be an object avoids misinterpreting user tables whose row struct happens to have scalar fields namedkeyandvalue. A residual false-positive remains for user tables whose top-level struct contains a nested struct namedkeyAND a field namedvalue— that shape is pinned in a regression test so the behavior is explicit.When
show_payeris set on a KV row,payeris captured intoram_payersthe same way as the legacy{data, payer}path. Missing payer fields pushundefinedrather than throwing (see the breaking-change note above).Tests
22 test cases in the two touched suites; full sdk-core suite is 311/311 green.
packages/sdk-core/tests/chain/Abi.test.tstable_id+ emptysecondary_indexesnamewas widenedsecondary_indexeswithchecksum256key_type(the case the wire-cdt fix(eslint): identity + no-swallow error law; enhance shared NestedError #49 abigen fix unblocked end-to-end)table_id0 and 65535 boundary valuestable_iddefaults to 0 on encode + decodeprotobuf_typesextension is consumed without affectingenumsprotobuf_typestrailer (round-trip validated, not a single-byte check)table_idoutside uint16 range (table + secondary index)sysio::abi/1.x(forward-compat)packages/sdk-core/tests/api/v1/Chain.test.ts{key, value}shape into plain rowsshow_payer+ capturesram_payers{data, payer}show_payershape from EOSIOundefinedkey+valuefields (first-order false-positive guard)keyis itself an object (pins the known residual false-positive)Out of scope
packages/sdk-core/src/resources/{Ram,Rex,Powerup}.tscallget_table_rowsfor tables (rammarket,rex_pool,powup_state) that do not exist in wire-sysio and have not for some time. These resources were already dead on Wire chains regardless of any of the in-flight wire-sysio PRs and remain dead after this commit.packages/sdk-core/src/types/SystemContractTypes.tsis auto-generated and would benefit from a regen pass after sysio.token's migration tokv::scoped_tablemerges (#291), but the field schemas of the structs it generates do not depend on the binarytable_deflayout.