Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions packages/sdk-core/src/api/v1/Chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,12 +345,59 @@ export class ChainAPI {
lower_bound
}
})
let ram_payers: Name[] | undefined
let ram_payers: (Name | undefined)[] | undefined

// Wire-sysio unified get_table_rows returns KV-backed rows as
// {key: {scope, primary_key, ...}, value: <decoded>, payer?: <name>}
// instead of the legacy shape (decoded struct directly, or
// {data, payer} when show_payer is set). Detect by shape: each row
// must be an object whose `key` is itself a plain object (the
// wire-sysio key is always a composite scope+primary_key struct)
// and also has a `value` field. Requiring `key` to be a non-array
// object avoids misinterpreting user tables that happen to have
// scalar or array fields named `key` and `value`.
//
// Residual ambiguity: a user-defined row whose top-level struct
// contains a nested struct named `key` AND a field named `value`
// will still trigger the KV unwrap and be silently stripped to
// `value`. Row shapes like `{key: {...}, value: ...}` are rare in
// practice, but cannot be fully distinguished from the wire KV
// shape by heuristic alone — the ABI would need to be consulted.
// See the test `does not unwrap user table whose key is itself an
// object` for a pinned repro of this known false-positive so the
// behavior is caught if the discriminant is ever tightened.
type WireKvRow = {
key: Record<string, unknown>
value: unknown
payer?: string
}

if (params.show_payer) {
const isWireKvShape =
Array.isArray(rows) &&
rows.length > 0 &&
typeof rows[0] === "object" &&
rows[0] !== null &&
"key" in rows[0] &&
"value" in rows[0] &&
typeof rows[0].key === "object" &&
rows[0].key !== null &&
!Array.isArray(rows[0].key)

if (isWireKvShape) {
if (params.show_payer) {
ram_payers = []
rows = rows.map((row: WireKvRow) => {
ram_payers!.push(row.payer ? Name.from(row.payer) : undefined)
return row.value
})
} else {
rows = rows.map((row: WireKvRow) => row.value)
}
} else if (params.show_payer) {
// Legacy show_payer wrapper: {data, payer}
ram_payers = []
rows = rows.map(({ data, payer }) => {
ram_payers!.push(Name.from(payer))
ram_payers!.push(payer ? Name.from(payer) : undefined)
return data
})
}
Expand Down
7 changes: 6 additions & 1 deletion packages/sdk-core/src/api/v1/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,12 @@ export interface GetTableRowsParamsTyped<
export interface GetTableRowsResponse<Index = TableIndexType, Row = any> {
rows: Row[]
more: boolean
ram_payers?: Name[]
/**
* Populated when `show_payer` is set. Each entry aligns with `rows[i]`.
* The entry is `undefined` when the chain returned a row without a payer
* (wire-sysio KV shape makes `payer` optional).
*/
ram_payers?: (Name | undefined)[]
next_key?: Index
}

Expand Down
127 changes: 123 additions & 4 deletions packages/sdk-core/src/chain/Abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ export class ABI implements ABISerializableObject {
const numTables = decoder.readVaruint32()

for (let i = 0; i < numTables; i++) {
const name = Name.fromABI(decoder)
// name is a length-prefixed string (widened from sysio::name uint64);
// table_id (uint16) and secondary_indexes (vector<index_def>) follow.
// Binary order matches sysio::chain::table_def.
const name = decoder.readString()
const index_type = decoder.readString()
const key_names: string[] = []
const numKeyNames = decoder.readVaruint32()
Expand All @@ -120,7 +123,29 @@ export class ABI implements ABISerializableObject {
}

const type = decoder.readString()
tables.push({ name, index_type, key_names, key_types, type })
const table_id = ABI.readUint16LE(decoder)
const secondary_indexes: ABI.Index[] = []
const numIndexes = decoder.readVaruint32()

for (let j = 0; j < numIndexes; j++) {
const idxName = decoder.readString()
const idxKeyType = decoder.readString()
secondary_indexes.push({
name: idxName,
key_type: idxKeyType,
table_id: ABI.readUint16LE(decoder)
})
}

tables.push({
name,
index_type,
key_names,
key_types,
type,
table_id,
secondary_indexes
})
}

const ricardian_clauses: ABI.Clause[] = []
Expand Down Expand Up @@ -214,6 +239,25 @@ export class ABI implements ABISerializableObject {
}
}

// After enums, wire-sysio sysio::abi/1.x writes protobuf_types as the
// sole trailing extension (length-prefixed string). We drain any
// further string-typed trailers for forward-compat within the 1.x
// line, but only when `version` is a recognized 1.x tag — if a
// future release bumps the major or introduces a non-string trailing
// extension, a blind drain would mis-parse the length prefix as a
// string length and silently produce a corrupt ABI. Scoping the
// drain to known-safe versions makes that failure mode explicit:
// unrecognized versions stop here with any trailing bytes ignored,
// and extending this parser is required when the wire format grows
// a new non-string field. The SDK does not consume the string
// trailers; they are read purely to advance past them.
const knownStringOnlyTrailers = version.startsWith("sysio::abi/1.")
if (knownStringOnlyTrailers) {
while (decoder.canRead()) {
decoder.readString()
}
}

return new ABI({
version,
types,
Expand Down Expand Up @@ -260,7 +304,11 @@ export class ABI implements ABISerializableObject {
encoder.writeVaruint32(this.tables.length)

for (const table of this.tables) {
Name.from(table.name).toABI(encoder)
// Binary order matches sysio::chain::table_def: name (string),
// index_type, key_names, key_types, type, table_id (uint16 LE),
// secondary_indexes (vector<index_def>). Mirror of the layout
// consumed in fromABI — keep both sides in sync.
encoder.writeString(table.name)
encoder.writeString(table.index_type)
encoder.writeVaruint32(table.key_names.length)

Expand All @@ -275,6 +323,25 @@ export class ABI implements ABISerializableObject {
}

encoder.writeString(table.type)
// table_id defaults to 0 for hand-built tables that omit it;
// on-chain ABIs always carry a DJB2(name) % 65536 value.
ABI.writeUint16LE(
encoder,
table.table_id ?? 0,
`table ${table.name} table_id`
)
const secIdx = table.secondary_indexes ?? []
encoder.writeVaruint32(secIdx.length)

for (const idx of secIdx) {
encoder.writeString(idx.name)
encoder.writeString(idx.key_type)
ABI.writeUint16LE(
encoder,
idx.table_id ?? 0,
`index ${idx.name} table_id`
)
}
}

encoder.writeVaruint32(this.ricardian_clauses.length)
Expand Down Expand Up @@ -329,6 +396,39 @@ export class ABI implements ABISerializableObject {
encoder.writeArray(buf)
}
}

// protobuf_types trailer: only emitted for sysio::abi/1.x, matching
// the decoder's version gate in fromABI. For non-1.x versions (e.g.
// a future sysio::abi/2.0 whose trailer shape is unknown here), no
// trailer is written so round-tripping through this SDK stays
// byte-exact for whatever version the ABI claims.
if (this.version.startsWith("sysio::abi/1.")) {
encoder.writeString("")
}
}

private static assertUint16(value: number, label: string) {
if (!Number.isInteger(value) || value < 0 || value > 0xffff) {
throw new Error(
`ABI ${label} must be a uint16 in [0, 65535], got ${value}`
)
}
}

private static readUint16LE(decoder: ABIDecoder): number {
const lo = decoder.readByte()
const hi = decoder.readByte()
return lo | (hi << 8)
}

private static writeUint16LE(
encoder: ABIEncoder,
value: number,
label: string
) {
ABI.assertUint16(value, label)
encoder.writeByte(value & 0xff)
encoder.writeByte((value >> 8) & 0xff)
}

resolveType(name: string): ABI.ResolvedType {
Expand Down Expand Up @@ -492,12 +592,31 @@ export namespace ABI {
type: string
ricardian_contract: string
}
// Per-secondary-index metadata embedded in Table. Mirrors
// sysio::chain::index_def. table_id is a uint16 DJB2(name) % 65536.
// On-chain ABIs always carry a non-zero table_id (CDT computes it at
// compile time); the field is optional here to allow programmatic
// construction of ABI objects, in which case the encoder writes 0.
export interface Index {
name: string
key_type: string
/** uint16 DJB2(name) % 65536; always present in on-chain ABIs,
* defaults to 0 on encode when omitted from a hand-built ABI. */
table_id?: number
}
// table_def.name is a free-form string (was sysio::name uint64 pre-wire),
// so names > 12 chars / containing arbitrary characters are valid.
// table_id (uint16) = DJB2(name) % 65536 and secondary_indexes provide
// per-table namespace isolation for KV-backed tables. This SDK only
// supports the wire-sysio binary format.
export interface Table {
name: NameType
name: string
index_type: string
key_names: string[]
key_types: string[]
type: string
table_id?: number
secondary_indexes?: Index[]
}
export interface Clause {
id: string
Expand Down
Loading