Broadcasting a transaction
The DEMOS transaction broadcasting system works as a 2 step process.
Step 1: Gas fee confirmation
The confirmation process starts by a client sending the transaction to the node. The node analyses the workload and returns information on how much gas is needed to execute your transaction.
const validityData = await demos.confirm(tx)
console.log("Validity data: ", validityData)
The information is wrapped in a validity data object, which looks something like this:
{
valid: true,
reference_block: 63754,
message: '[Native Tx Validation] Transaction signature verifiedn',
gas_operation: {
operator: 'pay_gas',
actor: '[object Object]',
params: {
amount: '10112.6664',
},
hash: '1150ffa293be58b3537fbc5c41c97ab3686a0360c85d64903dd0ecc3337ce713',
nonce: 0,
timestamp: 1726757110915,
status: 'pending',
fees: {
network_fee: 0,
rpc_fee: 0,
additional_fee: 0,
},
},
transaction: {
// ...
},
}
The response is an object containing the status code, the response or an error message if one occured and your transaction. If you are comfortable with the amount of gas to be used (gas_operation.params.amount), you can proceed to broadcasting the tx.
Your public key and public key signature are sent along with the request for verification. You need to have your KeyPair connected to the demos object.
Step 2: Broadcasting the validity data
To execute your transaction, send back the validity data to the node.
const res2 = await demos.broadcast(validityData)
Broadcast and wait for inclusion
demos.broadcast() returns as soon as the node accepts the transaction; it does not wait for the transaction to be included in a block. If you need a deterministic confirmation, use demos.broadcastAndWait() (added in v2.12.0). It broadcasts and then polls the node’s getTransactionStatus RPC until the transaction reaches a terminal state — included or failed — or the timeout elapses.
const { broadcast, status } = await demos.broadcastAndWait(validityData)
if (status.state === "included") {
console.log("included at block", status.blockNumber)
} else {
console.error("transaction failed at block", status.blockNumber)
}
The method accepts an optional options object:
| Option | Default | Purpose |
|---|
timeoutMs | 30000 | Total time to wait for inclusion before throwing BroadcastTimeoutError. |
pollIntervalMs | 500 | Delay between status polls. |
const result = await demos.broadcastAndWait(validityData, {
timeoutMs: 60_000,
pollIntervalMs: 1_000,
})
The lower-level DemosTransactions.broadcastAndWait(validityData, demos, opts) accepts an additional failFastOnBroadcastError flag. When set to true, the call throws BroadcastFailedError immediately if the broadcast itself can’t reach the node (e.g. ECONNREFUSED, ENOTFOUND). HTTP 5xx responses are not treated as fail-fast — the server did answer, so the tx may still have landed and the SDK keeps polling.
Error types
The transport layer surfaces a small, focused set of typed errors. Catch them by class to handle each case precisely:
| Error | Thrown by | Meaning |
|---|
TransportError | Any RPC method on Demos | The HTTP request to the node exhausted its retry budget. Carries attempts (total attempts including the final failing one) and cause (the underlying AxiosError). |
BroadcastTimeoutError | broadcastAndWait | The broadcast was accepted but no terminal state was observed before timeoutMs elapsed. Carries txHash, lastSeenState, and elapsedMs so callers can resume polling on their own. |
BroadcastFailedError | broadcastAndWait (when failFastOnBroadcastError: true) | The broadcast itself could not reach the node. Carries txHash and cause. |
SubDemPrecisionError | transfer, pay, sign (when amount has sub-DEM precision and node is pre-fork) | Sub-DEM precision would be silently truncated on the legacy wire. See Amounts & Denominations. Carries amountOs and subDemRemainderOs. |
import {
BroadcastTimeoutError,
BroadcastFailedError,
TransportError,
} from "@kynesyslabs/demosdk/websdk"
import { denomination } from "@kynesyslabs/demosdk"
// SubDemPrecisionError lives in the denomination namespace:
// denomination.SubDemPrecisionError
try {
const result = await demos.broadcastAndWait(validityData, { timeoutMs: 10_000 })
console.log("status:", result.status.state)
} catch (err) {
if (err instanceof BroadcastTimeoutError) {
console.warn(
`Timed out after ${err.elapsedMs}ms ` +
`(txHash=${err.txHash}, lastSeenState=${err.lastSeenState}). ` +
`Resume polling with demos.rpcCall('getTransactionStatus', { hash: err.txHash }).`
)
} else if (err instanceof TransportError) {
console.error(
`Transport gave up after ${err.attempts} attempts:`, err.cause
)
} else {
throw err
}
}
Retry semantics
The transport layer automatically retries on HTTP 5xx responses and honours Retry-After headers (seconds or HTTP-date). Backoff is capped at 8 seconds (MAX_BACKOFF_MS) so a misbehaving node cannot stall the SDK indefinitely. The attempts count surfaced by TransportError reflects the total number of attempts including the final failing one.
rpcCall and _doPost retry independently — _doPost retries on transport-level failures, rpcCall retries on RPC-level errors — so retry budgets do not multiply across layers.