Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71f7a22436 | |||
| 5319209cb2 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/parser2",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"description": "Universal EOSIO/Antelope SHiP-to-Redis blockchain indexer (parser)",
|
||||
"license": "MIT",
|
||||
"author": "Coopenomics contributors",
|
||||
|
||||
@@ -97,8 +97,13 @@ export class BlockProcessor {
|
||||
|
||||
const blockNum = block.thisBlock.blockNum
|
||||
const blockId = block.thisBlock.blockId
|
||||
// blockTime берём из первой трассировки; если трассировок нет — текущее время
|
||||
const blockTime = block.traces[0]?.blockTime ?? new Date().toISOString()
|
||||
// block_time — on-chain время блока (signed_block.timestamp), общее для всех событий
|
||||
// блока, включая trace-less блоки (genesis). НЕ wall-clock: подставлять new Date()
|
||||
// нельзя — это ломает point-in-time запросы. Пустое = block не запрошен у SHiP.
|
||||
const blockTime = block.blockTime
|
||||
if (!blockTime) {
|
||||
this.log.warn({ block_num: blockNum }, 'block_time missing from ship block (fetch_block disabled?)')
|
||||
}
|
||||
|
||||
// ── Фаза 1: Action traces ─────────────────────────────────────────────────
|
||||
for (const trace of block.traces) {
|
||||
|
||||
@@ -51,6 +51,7 @@ function makeBlock(blockNum = 1, numTraces = 0, numDeltas = 0): ShipBlock {
|
||||
head: blockPosition,
|
||||
lastIrreversible: blockPosition,
|
||||
prevBlock: null,
|
||||
blockTime: '2024-06-01T12:00:00.000',
|
||||
traces: Array.from({ length: numTraces }, (_, i) => ({
|
||||
account: 'eosio.token',
|
||||
name: 'transfer',
|
||||
@@ -175,6 +176,7 @@ describe('BlockProcessor — ABI updates (Story 4.3)', () => {
|
||||
head: { blockNum: 500, blockId: 'c'.repeat(64) },
|
||||
lastIrreversible: { blockNum: 500, blockId: 'c'.repeat(64) },
|
||||
prevBlock: null,
|
||||
blockTime: '2024-06-01T12:00:00.000',
|
||||
traces: [{
|
||||
account: 'eosio',
|
||||
name: 'setabi',
|
||||
@@ -214,6 +216,7 @@ describe('BlockProcessor — ABI updates (Story 4.3)', () => {
|
||||
head: { blockNum: 501, blockId: 'c'.repeat(64) },
|
||||
lastIrreversible: { blockNum: 501, blockId: 'c'.repeat(64) },
|
||||
prevBlock: null,
|
||||
blockTime: '2024-06-01T12:00:00.000',
|
||||
traces: [{
|
||||
account: 'eosio',
|
||||
name: 'setabi',
|
||||
@@ -253,6 +256,7 @@ describe('BlockProcessor — ABI updates (Story 4.3)', () => {
|
||||
head: { blockNum: 600, blockId: 'e'.repeat(64) },
|
||||
lastIrreversible: { blockNum: 600, blockId: 'e'.repeat(64) },
|
||||
prevBlock: null,
|
||||
blockTime: '2024-06-01T12:00:00.000',
|
||||
traces: [],
|
||||
deltas: [{
|
||||
name: 'account' as never,
|
||||
@@ -283,6 +287,7 @@ describe('BlockProcessor — native deltas (Epic 6)', () => {
|
||||
head: { blockNum: 10, blockId: 'a'.repeat(64) },
|
||||
lastIrreversible: { blockNum: 10, blockId: 'a'.repeat(64) },
|
||||
prevBlock: null,
|
||||
blockTime: '2024-06-01T12:00:00.000',
|
||||
traces: [],
|
||||
deltas: nativeTableNames.map(name => ({
|
||||
name: name as never,
|
||||
@@ -338,6 +343,7 @@ describe('BlockProcessor — native deltas (Epic 6)', () => {
|
||||
head: { blockNum: 20, blockId: 'b'.repeat(64) },
|
||||
lastIrreversible: { blockNum: 20, blockId: 'b'.repeat(64) },
|
||||
prevBlock: null,
|
||||
blockTime: '2024-06-01T12:00:00.000',
|
||||
traces: [
|
||||
{ account: 'eosio.token', name: 'transfer', authorization: [], actRaw: new Uint8Array([1]), actionOrdinal: 1, creatorActionOrdinal: 0, globalSequence: BigInt(1), receipt: null, contextFree: false, elapsed: 0, console: '', accountRamDeltas: [], blockNum: 20, blockId: 'b'.repeat(64), blockTime: '2024-01-01T00:00:00.000', transactionId: 'c'.repeat(64) },
|
||||
{ account: 'eosio.token', name: 'transfer', authorization: [], actRaw: new Uint8Array([1]), actionOrdinal: 2, creatorActionOrdinal: 0, globalSequence: BigInt(2), receipt: null, contextFree: false, elapsed: 0, console: '', accountRamDeltas: [], blockNum: 20, blockId: 'b'.repeat(64), blockTime: '2024-01-01T00:00:00.000', transactionId: 'c'.repeat(64) },
|
||||
@@ -370,6 +376,7 @@ describe('BlockProcessor — native deltas (Epic 6)', () => {
|
||||
head: { blockNum: 5, blockId: 'd'.repeat(64) },
|
||||
lastIrreversible: { blockNum: 5, blockId: 'd'.repeat(64) },
|
||||
prevBlock: null,
|
||||
blockTime: '2024-06-01T12:00:00.000',
|
||||
traces: [{ account: 'eosio', name: 'newaccount', authorization: [], actRaw: new Uint8Array([1]), actionOrdinal: 1, creatorActionOrdinal: 0, globalSequence: BigInt(10), receipt: null, contextFree: false, elapsed: 0, console: '', accountRamDeltas: [], blockNum: 5, blockId: 'd'.repeat(64), blockTime: '2024-01-01T00:00:00.000', transactionId: 'e'.repeat(64) }],
|
||||
deltas: [
|
||||
{ name: 'contract_row', present: true, rowRaw: new Uint8Array([1]), code: 'eosio', scope: 'eosio', table: 'global', primaryKey: '0' },
|
||||
@@ -390,6 +397,7 @@ describe('BlockProcessor — native deltas (Epic 6)', () => {
|
||||
head: { blockNum: 700, blockId: 'f'.repeat(64) },
|
||||
lastIrreversible: { blockNum: 700, blockId: 'f'.repeat(64) },
|
||||
prevBlock: null,
|
||||
blockTime: '2024-06-01T12:00:00.000',
|
||||
traces: [],
|
||||
deltas: [{ name: 'account' as never, present: true, rowRaw: new Uint8Array([1, 2, 3]) }],
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/coopos-ship-reader",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"description": "Clean-room SHiP WebSocket client for EOSIO/Antelope blockchains",
|
||||
"license": "MIT",
|
||||
"author": "Coopenomics contributors",
|
||||
|
||||
@@ -79,7 +79,6 @@ export async function* createBlockStream(
|
||||
}
|
||||
|
||||
let blockNum = opts.startBlock
|
||||
let blockTime = new Date().toISOString()
|
||||
|
||||
while (true) {
|
||||
const msg = await nextMessage()
|
||||
@@ -91,7 +90,7 @@ export async function* createBlockStream(
|
||||
const [type, raw] = decodeResult(new Uint8Array(msg), abi)
|
||||
if (type !== 'get_blocks_result_v0') continue
|
||||
|
||||
const block = decodeBlocksResult(raw, abi, blockNum, '', blockTime)
|
||||
const block = decodeBlocksResult(raw, abi, blockNum, '')
|
||||
blockNum = block.thisBlock.blockNum
|
||||
|
||||
yield block
|
||||
|
||||
@@ -43,6 +43,7 @@ interface RawBlocksResult {
|
||||
last_irreversible: RawBlockPos
|
||||
this_block: RawBlockPos | null
|
||||
prev_block: RawBlockPos | null
|
||||
block: Bytes | null
|
||||
traces: Bytes | null
|
||||
deltas: Bytes | null
|
||||
}
|
||||
@@ -113,7 +114,7 @@ export function decodeStatusResult(raw: unknown): { chainId: string; head: Block
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeBlocksResult(raw: unknown, abi: ShipAbi, blockNum: number, blockId: string, blockTime: string): ShipBlock {
|
||||
export function decodeBlocksResult(raw: unknown, abi: ShipAbi, blockNum: number, blockId: string): ShipBlock {
|
||||
const r = raw as RawBlocksResult
|
||||
|
||||
// ABI decoder возвращает checksum256 как Checksum256 объект (не строку) —
|
||||
@@ -127,6 +128,22 @@ export function decodeBlocksResult(raw: unknown, abi: ShipAbi, blockNum: number,
|
||||
? { blockNum: r.prev_block.block_num, blockId: String(r.prev_block.block_id) }
|
||||
: null
|
||||
|
||||
// Время блока берём из on-chain header (signed_block.timestamp), а НЕ из wall-clock.
|
||||
// r.block — optional<bytes> с сериализованным signed_block, его база — block_header,
|
||||
// первое поле которого timestamp. Декодим как block_header: читаются только
|
||||
// header-поля, тело блока (transactions) не десериализуем.
|
||||
let blockTime = ''
|
||||
if (r.block && r.block.length > 0) {
|
||||
try {
|
||||
const headerDecoded = Serializer.decode({ data: r.block.array, type: 'block_header', abi })
|
||||
const header = Serializer.objectify(headerDecoded) as { timestamp?: string }
|
||||
blockTime = header.timestamp ? String(header.timestamp) : ''
|
||||
} catch {
|
||||
// Не смогли распарсить header — оставляем пустым, downstream залогирует.
|
||||
blockTime = ''
|
||||
}
|
||||
}
|
||||
|
||||
const txTraces = decodeVector<[string, RawTransactionTrace]>(r.traces, 'transaction_trace', abi)
|
||||
const tableDeltaVariants = decodeVector<[string, RawTableDelta]>(r.deltas, 'table_delta', abi)
|
||||
|
||||
@@ -228,5 +245,5 @@ export function decodeBlocksResult(raw: unknown, abi: ShipAbi, blockNum: number,
|
||||
}
|
||||
}
|
||||
|
||||
return { thisBlock, head, lastIrreversible, prevBlock, traces, deltas }
|
||||
return { thisBlock, head, lastIrreversible, prevBlock, blockTime, traces, deltas }
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ export interface ShipBlock {
|
||||
readonly head: BlockPosition
|
||||
readonly lastIrreversible: BlockPosition
|
||||
readonly prevBlock: BlockPosition | null
|
||||
/** On-chain время блока (signed_block.timestamp), ISO-строка. '' если block не запрошен. */
|
||||
readonly blockTime: string
|
||||
readonly traces: readonly ShipTrace[]
|
||||
readonly deltas: readonly ShipDelta[]
|
||||
}
|
||||
|
||||
@@ -123,6 +123,13 @@ function makeShipAbi(): ABI {
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'rows', type: 'row[]' },
|
||||
] },
|
||||
// Упрощённый block_header: реальный signed_block начинается с этих байт,
|
||||
// decodeBlocksResult декодит r.block как 'block_header' и читает timestamp.
|
||||
// Достаточно первых двух полей — self-consistent encode/decode тем же ABI.
|
||||
{ name: 'block_header', base: '', fields: [
|
||||
{ name: 'timestamp', type: 'block_timestamp_type' },
|
||||
{ name: 'producer', type: 'name' },
|
||||
] },
|
||||
],
|
||||
actions: [],
|
||||
tables: [],
|
||||
@@ -186,7 +193,7 @@ describe('ShipProtocol — decodeBlocksResult (no traces / no deltas)', () => {
|
||||
traces: null,
|
||||
deltas: null,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 100, 'c'.repeat(64), '2024-01-01T00:00:00.000')
|
||||
const block = decodeBlocksResult(raw, abi, 100, 'c'.repeat(64))
|
||||
expect(block.traces).toHaveLength(0)
|
||||
expect(block.deltas).toHaveLength(0)
|
||||
expect(block.thisBlock.blockNum).toBe(100)
|
||||
@@ -202,7 +209,7 @@ describe('ShipProtocol — decodeBlocksResult (no traces / no deltas)', () => {
|
||||
traces: null,
|
||||
deltas: null,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 42, 'fallback-id', '2024-01-01T00:00:00.000')
|
||||
const block = decodeBlocksResult(raw, abi, 42, 'fallback-id')
|
||||
expect(block.thisBlock.blockNum).toBe(42)
|
||||
expect(block.thisBlock.blockId).toBe('fallback-id')
|
||||
expect(block.prevBlock).toBeNull()
|
||||
@@ -219,7 +226,7 @@ describe('ShipProtocol — decodeBlocksResult (no traces / no deltas)', () => {
|
||||
traces: null,
|
||||
deltas: null,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 100, 'fallback', '2024-01-01T00:00:00.000')
|
||||
const block = decodeBlocksResult(raw, abi, 100, 'fallback')
|
||||
expect(typeof block.thisBlock.blockId).toBe('string')
|
||||
expect(typeof block.head.blockId).toBe('string')
|
||||
expect(typeof block.lastIrreversible.blockId).toBe('string')
|
||||
@@ -316,7 +323,7 @@ describe('ShipProtocol — decodeBlocksResult (real decoded traces)', () => {
|
||||
deltas: null,
|
||||
}
|
||||
|
||||
const block = decodeBlocksResult(raw, abi, 200, 'c'.repeat(64), '2024-01-01T00:00:00.000')
|
||||
const block = decodeBlocksResult(raw, abi, 200, 'c'.repeat(64))
|
||||
expect(block.traces).toHaveLength(1)
|
||||
|
||||
const t = block.traces[0]!
|
||||
@@ -346,7 +353,7 @@ describe('ShipProtocol — decodeBlocksResult (real decoded traces)', () => {
|
||||
traces: tracesBytes,
|
||||
deltas: null,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 200, 'c'.repeat(64), '2024-01-01T00:00:00.000')
|
||||
const block = decodeBlocksResult(raw, abi, 200, 'c'.repeat(64))
|
||||
const r = block.traces[0]!.receipt!
|
||||
expect(typeof r.receiver).toBe('string')
|
||||
expect(typeof r.actDigest).toBe('string')
|
||||
@@ -367,7 +374,7 @@ describe('ShipProtocol — decodeBlocksResult (real decoded traces)', () => {
|
||||
traces: tracesBytes,
|
||||
deltas: null,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 200, 'c'.repeat(64), '2024-01-01T00:00:00.000')
|
||||
const block = decodeBlocksResult(raw, abi, 200, 'c'.repeat(64))
|
||||
expect(block.traces[0]!.globalSequence).toBe(42n)
|
||||
})
|
||||
|
||||
@@ -382,7 +389,7 @@ describe('ShipProtocol — decodeBlocksResult (real decoded traces)', () => {
|
||||
traces: tracesBytes,
|
||||
deltas: null,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 200, 'c'.repeat(64), '2024-01-01T00:00:00.000')
|
||||
const block = decodeBlocksResult(raw, abi, 200, 'c'.repeat(64))
|
||||
expect(block.traces[0]!.receipt).toBeNull()
|
||||
// Без receipt и top-level global_sequence — fallback на 0n
|
||||
expect(block.traces[0]!.globalSequence).toBe(0n)
|
||||
@@ -399,7 +406,7 @@ describe('ShipProtocol — decodeBlocksResult (real decoded traces)', () => {
|
||||
traces: tracesBytes,
|
||||
deltas: null,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 200, 'c'.repeat(64), '2024-01-01T00:00:00.000')
|
||||
const block = decodeBlocksResult(raw, abi, 200, 'c'.repeat(64))
|
||||
expect(block.traces[0]!.actRaw).toBeInstanceOf(Uint8Array)
|
||||
})
|
||||
})
|
||||
@@ -435,7 +442,7 @@ describe('ShipProtocol — decodeBlocksResult (deltas)', () => {
|
||||
traces: null,
|
||||
deltas: tableDelta,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 300, 'c'.repeat(64), '2024-01-01T00:00:00.000')
|
||||
const block = decodeBlocksResult(raw, abi, 300, 'c'.repeat(64))
|
||||
expect(block.deltas).toHaveLength(1)
|
||||
const d = block.deltas[0]!
|
||||
expect(d.name).toBe('contract_row')
|
||||
@@ -470,7 +477,7 @@ describe('ShipProtocol — decodeBlocksResult (deltas)', () => {
|
||||
traces: null,
|
||||
deltas: tableDelta,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 300, 'c'.repeat(64), '2024-01-01T00:00:00.000')
|
||||
const block = decodeBlocksResult(raw, abi, 300, 'c'.repeat(64))
|
||||
expect(block.deltas).toHaveLength(1)
|
||||
const d = block.deltas[0]!
|
||||
expect(d.name).toBe('permission')
|
||||
@@ -499,12 +506,51 @@ describe('ShipProtocol — decodeBlocksResult (deltas)', () => {
|
||||
traces: null,
|
||||
deltas: tableDelta,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 300, 'c'.repeat(64), '2024-01-01T00:00:00.000')
|
||||
const block = decodeBlocksResult(raw, abi, 300, 'c'.repeat(64))
|
||||
// Некорректная row пропущена без throw
|
||||
expect(block.deltas).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ShipProtocol — decodeBlocksResult (block_time из header)', () => {
|
||||
// Регресс-гард на баг #2: block_time брался из wall-clock (new Date()),
|
||||
// одинаковый для всех блоков. Теперь — из on-chain signed_block.timestamp.
|
||||
it('extracts on-chain block_time from signed_block header (not wall-clock)', () => {
|
||||
const abi = makeShipAbi()
|
||||
const headerBytes = Serializer.encode({
|
||||
object: { timestamp: '2026-05-31T18:01:22.000', producer: 'eosio' },
|
||||
type: 'block_header',
|
||||
abi,
|
||||
})
|
||||
const raw = {
|
||||
head: { block_num: 200, block_id: 'a'.repeat(64) },
|
||||
last_irreversible: { block_num: 190, block_id: 'b'.repeat(64) },
|
||||
this_block: { block_num: 200, block_id: 'c'.repeat(64) },
|
||||
prev_block: null,
|
||||
block: headerBytes,
|
||||
traces: null,
|
||||
deltas: null,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 200, 'c'.repeat(64))
|
||||
expect(block.blockTime).toContain('2026-05-31T18:01:22')
|
||||
})
|
||||
|
||||
it('block_time is empty when block not fetched (fetch_block disabled)', () => {
|
||||
const abi = makeShipAbi()
|
||||
const raw = {
|
||||
head: { block_num: 100, block_id: 'a'.repeat(64) },
|
||||
last_irreversible: { block_num: 90, block_id: 'b'.repeat(64) },
|
||||
this_block: { block_num: 100, block_id: 'c'.repeat(64) },
|
||||
prev_block: null,
|
||||
block: null,
|
||||
traces: null,
|
||||
deltas: null,
|
||||
}
|
||||
const block = decodeBlocksResult(raw, abi, 100, 'c'.repeat(64))
|
||||
expect(block.blockTime).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ShipProtocol — defence against wharfkit returning typed objects', () => {
|
||||
// Регрессионный тест: если кто-то удалит String() обёртки — эти тесты упадут.
|
||||
it('all trace string fields survive JSON.stringify roundtrip without [object Object]', () => {
|
||||
|
||||
Reference in New Issue
Block a user