Skip to main content

Async Operations

All blockchain operations (deploy, mint, transfer, burn, etc.) are asynchronous. The API returns immediately while the operation is processed in the background. This design is necessary because blockchain transactions can take seconds to minutes to confirm.

Transaction Lifecycle

┌─────────┐     ┌───────────┐     ┌───────────┐     ┌───────────┐
│ pending │ ──→ │ submitted │ ──→ │ confirmed │ or │ failed │
└─────────┘ └───────────┘ └───────────┘ └───────────┘
↓ ↓ ↓ ↓
Queued for Sent to RPC Included in Reverted or
processing (has tx_hash) a block timed out
StatusMeaningHas tx_hash?
pendingJob queued, not yet submittedNo
submittedSent to blockchain RPCYes
confirmedIncluded in a block and confirmedYes
failedReverted or could not be submittedSometimes
Token Deploys

Token deployment uses "deploying" instead of "pending" as the initial status to distinguish from regular transaction operations.

Request Flow

1. Client sends POST request
→ API validates input (returns 400 if invalid)
→ Saves transaction to PostgreSQL with status: "pending"
→ Enqueues job to BullMQ
→ Returns { id: "tx_abc123", status: "pending" }

2. tx-submit worker picks up job
→ Builds transaction (to, data, gas, nonce)
→ Signs with tenant's HD wallet
→ Submits to blockchain RPC
→ Updates status: "submitted", sets tx_hash

3. tx-confirm worker monitors
→ Polls for block inclusion
→ Updates status: "confirmed" (with block_number, gas_used)
→ Fires webhook event: "transaction.confirmed"

Tracking Transaction Status

Option 1: Polling

const tx = await urblock.tokens.mint("tok_abc123", {
to: "0x1234...",
amount: "1000000000000000000",
idempotency_key: "mint-001",
});

// Poll until final state
let result = tx;
while (result.status === "pending" || result.status === "submitted") {
await new Promise(r => setTimeout(r, 3000)); // Wait 3 seconds
result = await urblock.transactions.get(tx.id);
}

if (result.status === "confirmed") {
console.log(`Confirmed in block ${result.block_number} — tx: ${result.tx_hash}`);
} else {
console.error(`Failed: ${result.error_message}`);
}
// Register a webhook (one-time setup)
await urblock.webhooks.create({
url: "https://api.example.com/webhooks/urblock",
events: ["transaction.confirmed", "transaction.failed"],
});

// Your webhook handler receives events in real time
app.post("/webhooks/urblock", (req, res) => {
const event = req.body;

if (event.type === "transaction.confirmed") {
console.log(`TX ${event.data.id} confirmed — hash: ${event.data.tx_hash}`);
} else if (event.type === "transaction.failed") {
console.error(`TX ${event.data.id} failed — ${event.data.error_message}`);
}

res.status(200).send("ok");
});

Retry Behavior

Workers automatically retry failed jobs with exponential backoff:

OperationMax RetriesBase DelayMax Delay
Transaction submit35s40s
Transaction confirm1010s~2.8h
Contract deploy310s80s
Webhook delivery530s~32 min

Timeout Expectations

NetworkAverage ConfirmationMax Wait
Polygon~2-5 seconds30s
Ethereum~12-15 seconds5 min
Arbitrum~1-3 seconds30s
Base~2-4 seconds30s

Best Practices

  1. Use webhooks for production — polling is fine for dev, but webhooks are more efficient
  2. Include idempotency keys — safe retries if your client times out before getting the response
  3. Don't assume immediate confirmation — always check final status
  4. Handle failures gracefully — reverted transactions are normal; implement retry logic
  5. Log transaction IDs — store tx_id in your database to correlate with webhook events

Next Steps