Raw transactions on /tx
The /tx stream sends each signed Robinhood Chain transaction as one binary record, as soon as its bytes complete, usually before its block completes. You need no node and no decoder of your own.
Plan
/tx is part of the Ultra plan. Ask for a key on Telegram.
Endpoint
wss://tx.first-light.stream/{KEY}The host selects the stream, and the key is the whole path. The key must name the tx stream. See keys.
How a block reaches you
On the sequencer feed, a block is an entry: a sequence number, a timestamp, the block hash that the sequencer claims, and an ordered list of signed transactions.
- For each transaction that completes early, you get a Transaction record with the provisional flag. Ordinals start at 0 and increase by one.
- When the entry is complete, you get a Transaction record without the provisional flag for each transaction that the provisional records did not cover.
- Then you get exactly one EntryComplete record for the sequence. It confirms the provisional records. Its
tx_countis the number of transactions in the block.
The record
Each record is one binary WebSocket frame. A frame is never fragmented. The relay never sends a text frame on /tx. All integers are little-endian. Each record has a 28-byte header, then the payload.
| Offset | Type | Field | Meaning |
|---|---|---|---|
| 0 | u8 | version | 1 |
| 1 | u8 | kind | 1 Transaction, 2 EntryComplete, 3 Dropped, 4 Gap |
| 2 | u8 | flags | bit 0: provisional. bit 1: hash present. Bits 2 to 7 are zero. |
| 3 | u8 | tx_type | Kind 1: 0 for a legacy transaction, else the EIP-2718 type byte. Else 0. |
| 4 | u32 | ordinal | Kind 1: the transaction's index in the block. Else 0. |
| 8 | u64 | sequence | The feed's sequence number. Kind 4: the first sequence after the gap. |
| 16 | u64 | received_unix_ns | The relay's stamp, in nanoseconds, of the read that completed these bytes (kinds 1 and 2). 0 for kinds 3 and 4. |
| 24 | u32 | payload_len | The length of the payload in bytes. |
Payloads
- Kind 1, Transaction
- The exact signed transaction bytes (the EIP-2718 envelope, or the RLP list of a legacy transaction). keccak-256 of these bytes is the transaction hash. If flag bit 1 is set, the 32 bytes of the hash follow, inside
payload_len. - Kind 2, EntryComplete
- 44 bytes:
u32 tx_count,u64 sequencer_timestamp(whole seconds),[u8; 32] block_hash. A block hash of 32 zero bytes means that the sequencer sent none. - Kind 3, Dropped
- 1 byte:
u8 reason. 1: the entry was malformed. 2: a later entry, a gap or a malformed entry passed it. 3: the relay abandoned it. Discard the provisional transactions of the sequence. - Kind 4, Gap
- 8 bytes:
u64 expected. The relay never saw the sequences fromexpectedup to, but not including,sequence.
What you can rely on
- Within a sequence, the order is: provisional Transaction records, then the other Transaction records, then one EntryComplete.
- Records of different sequences can interleave, because more than one sequence can be open at a time.
- A Dropped comes only for a sequence whose provisional records you received. Any reason means: discard them. After reason 3, the entry can still arrive, and then it arrives whole: all its transactions again from ordinal 0, without the provisional flag, then EntryComplete. Count only the Transaction records after the Dropped against
tx_count. - An open sequence below a Gap gets its Dropped (reason 2) before the Gap record.
- EntryComplete sequences increase. They are not always consecutive.
- A new connection receives only sequences that start after it connected. It never joins in the middle of a sequence.
tx_countcounts the signed transactions that the entry carried. It does not count what the chain executed.- The stream has no history and no filters, and it does not recover the sender. To get
from, recover it from the signature.
Request and response headers
- Send
X-Rhc-Hash: 1on the upgrade request to get the hash on every Transaction record (flag bit 1). - The
101answer carriesX-Rhc-Record-Version: 1. Check it, and check byte 0 of each record. Refuse a version that you do not know. - The
101answer carriesX-Rhc-Relay-Epoch, which identifies the relay process. Tworeceived_unix_nsstamps of one epoch subtract to an interval. After a relay restart, sequences can repeat: deduplicate by sequence.
Versions
- A frame is one record. Its length is
28 + payload_len. - Skip a
kindthat you do not know, bypayload_len. - A flag that you did not ask for and do not know is a protocol error.
- A change to the header or to an existing payload would be version 2. A version 2 would be served beside version 1, and we would tell you its URL.
Connect
websocat
websocat -b -H 'X-Rhc-Hash: 1' 'wss://tx.first-light.stream/{KEY}' | xxd | headNode.js
import WebSocket from "ws";
const ws = new WebSocket(`wss://tx.first-light.stream/${process.env.KEY}`, {
headers: { "X-Rhc-Hash": "1" },
});
ws.on("upgrade", (res) => {
if (res.headers["x-rhc-record-version"] !== "1") throw new Error("not /tx version 1");
});
ws.on("message", (b, isBinary) => {
if (!isBinary || b[0] !== 1) return;
const kind = b[1], flags = b[2];
const sequence = b.readBigUInt64LE(8);
const payload = b.subarray(28, 28 + b.readUInt32LE(24));
if (kind === 1) {
const ordinal = b.readUInt32LE(4);
const provisional = (flags & 1) === 1;
const tx = flags & 2 ? payload.subarray(0, payload.length - 32) : payload;
const hash = flags & 2 ? payload.subarray(payload.length - 32) : null;
console.log("tx", sequence, ordinal, provisional, hash?.toString("hex"), tx.length);
} else if (kind === 2) {
console.log("complete", sequence, payload.readUInt32LE(0), "transactions");
} else if (kind === 3) {
console.log("dropped", sequence, "reason", payload[0]);
} else if (kind === 4) {
console.log("gap", payload.readBigUInt64LE(0), "to", sequence);
}
});The ws package answers the relay's pings for you. A client that reads frames by hand must answer each ping with a masked pong.
Limits and errors
The /tx stream has the same refusals, connection limits, pings and close codes as the feed. It does not negotiate permessage-deflate. On any reconnect, discard each sequence that is still open: the new connection does not tell you more of it.
Related pages
- Headers on
/headers: the Lead as a block starts, and the Header after. - FirstLight Submit
- The feed
- The plans