Skip to content

Broadcasting a Transaction

With some coins in hand, we can build and sign a Transaction that spends them, with DartSV or Bitcoin4J, and hand it to Arcade. The Bitcoin4J tutorial and the library docs cover the building. In this part we look at what happens once the Transaction leaves our application.

Broadcasting and merkle proofs in the Developer Guide explains why we broadcast through a service like Arcade, and what a wallet does with the proof. This page is the hands-on version, against our local network.

We POST the Transaction to /tx, in Extended Format. Arcade accepts it as JSON, raw bytes or hex:

Terminal window
curl -s -X POST http://127.0.0.1:23011/tx \
-H 'Content-Type: application/json' \
-d '{"rawTx":"<extended format hex>"}'
{"txid":"…","status":202,"txStatus":"RECEIVED"}

Notice that there is no /v1 in the address. Arcade serves the ARC API at its root.

Arcade answers 202 RECEIVED at once, every time, and takes the Transaction to the network afterwards. That reply only means Arcade has accepted and checked it. To find out how it went, we have to ask again.

A plain raw Transaction is refused with 460 Not extended format. This is the most common surprise when one first points an existing ARC client at Arcade.

Terminal window
curl -s http://127.0.0.1:23011/tx/<txid>
{"txid":"…","txStatus":"MINED","blockHash":"…","blockHeight":103,"merklePath":"<BUMP hex>"}

On the local network, a Transaction moves through these steps:

Status When What it means
RECEIVED in the reply to POST /tx Arcade accepted and checked it
ACCEPTED_BY_NETWORK after about 2 seconds Teranode took it
SEEN_ON_NETWORK after about 2 to 8 seconds it is in a subtree, i.e. in the next block the node will mine
MINED about 3 seconds after the next block it is in a block, and merklePath holds its proof

A Transaction can also end up REJECTED, with an ARC error code from 460 to 476 and the reason in extraInfo, or DOUBLE_SPEND_ATTEMPTED, with the competing Transactions in competingTxs. PENDING_RETRY means Arcade will try the broadcast again by itself.

Do note that Arcade only knows the Transactions that were submitted to it. Asking about any other txid gives us 404 {"error":"transaction not found"}, even if the Transaction is in a block.

Asking over and over is the simplest, but not the only option.

  • Polling GET /tx/{txid}, as above.
  • Server-sent events. If we submit with an X-CallbackToken header, GET http://127.0.0.1:23013/events?callbackToken=<token> streams every status change of every Transaction submitted with that token. The MINED events carry the merkle path. When we reconnect, we send the last event’s id as Last-Event-ID, and Arcade replays what we missed.
  • A webhook. If we submit with an X-CallbackUrl header, Arcade POSTs the status to that URL, with Authorization: Bearer <X-CallbackToken>. By default it only sends the final ones (MINED, REJECTED, DOUBLE_SPEND_ATTEMPTED, IMMUTABLE), and X-FullStatusUpdates: true asks for every change.

The webhook URL has to be reachable from the Arcade container, not from our machine. A service running on the host is http://host.docker.internal:<port>/….

A reorg that moves a Transaction to another block sends MINED again, with the new proof.

The merklePath is a BUMP (BRC-74) in hex, and it has to compute the merkle root in the header of the block at blockHeight. The stack comes with a script that checks exactly that:

Terminal window
./scripts/verify-proof.py <txid>

It prints PROOF VALID, or PROOF DOES NOT MATCH. Our own wallet code should do the same check against its own headers before it stores a proof, and the next part shows where to get those headers from.