# WebSocket

> NeuroChain gateway WebSocket——连接 /ws、init 消息、所有实时事件（区块、交易、被暂扣的转账、AI 决策、合约告警）、余额订阅、通过 socket 调用 RPC 以及适用的限制。

Source: https://docs.nro.world/zh/reference/websocket

Gateway 在与其 REST API 相同的主机和端口上，于 **`/ws`** 提供 WebSocket 服务：

```
ws://localhost:3000/ws
```

无需认证。Gateway 会向每个已连接的客户端推送链上事件和 AI 事件；客户端也可以在同一个 socket 上订阅余额并调用一组 RPC 方法。

## 连接

```js
const ws = new WebSocket('ws://localhost:3000/ws')

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data)
  switch (msg.type) {
    case 'init':      console.log('height', msg.data.height); break
    case 'new_block': console.log('block', msg.data.height, msg.data.proposer); break
    case 'tx_held':   console.warn('held', msg.data.tx_hash); break
  }
}
```

服务器发出的每条消息都是带有 `type` 的 JSON。事件把载荷放在 `data` 中；`balance`、`rpc_reply` 和 `error` 则把各自的字段放在顶层。

在 `close` 时以退避策略重连——Web 应用会以指数增长的延迟重试。

## 事件

Gateway 每隔 `POLL_NODE_INTERVAL_MS`（默认 2 000 ms）轮询一次节点，并把观察到的变化转换为事件。

| `type` | 触发时机 | `data` 字段 |
|--------|------|---------------|
| `init` | 连接建立后立即发送 | `height`、`tps`、`finality_ms`、`shard_count`、`validator_count`、`blocks`、`txs`、`total_blocks`、`total_txs` |
| `metrics` | 每次轮询 | `tps`、`height`、`finality_ms` |
| `new_block` | 出现新的高度 | 与 `GET /api/v1/blocks/:height` 返回的区块相同 |
| `validator_update` | 每 10 个区块 | `nodes`、`total_staked`、`height` |
| `tx` | Gateway 已将一笔交易转发给节点 | `hash`、`from`、`to`、`value`、`gas`、`gas_price`、`shard`、`timestamp`、`block` |
| `faucet` | 水龙头向某地址发放了资金 | `address`、`amount`、`tx_hash` |
| `tx_held` | 一笔转账被 [Sentinel 策略](/zh/ai/sentinel)暂扣 | `tx_hash`、`to`、`value_human` |
| `tx_released` | 一笔被暂扣的转账已放行 | `tx_hash`、`to`、`value_human` |
| `tx_cancelled` | 一笔被暂扣的转账已取消 | `tx_hash`、`to`、`value_human` |
| `tx_anomaly` | Sentinel 将一笔已提交的交易判定为异常 | `tx_hash`、`score`、`category` |
| `oracle_decision` | 记录了一条 AI 决策 | `model`、`kind`、`subject`、`decision`、`confidence`、`timestamp` |
| `model_voting_update` | 有 AI 模型正在等待验证者确认 | `models`、`height` |
| `contract_alert` | 某合约调用量异常，或某低分合约开始被调用 | `address`、`alert`、`severity`、`call_count`、`delta`、`timestamp`、`model` |

::: tip `tx` 只是提交通知
`tx` 事件在 gateway 把交易交给节点时触发，此时交易尚未进入区块——其 `status` 字段不具权威性。请通过 `new_block`、余额订阅或 `GET /api/v1/transactions/:hash` 跟踪最终性。
:::

合约告警每隔 `SENTINEL_CHECK_INTERVAL_MS`（默认 30 000 ms）检查一次。

## 余额订阅

```js
ws.send(JSON.stringify({ type: 'subscribe_balance', address: 'neuro:<15 hex>.human' }))
```

Gateway 会立即回复一次，之后每个新区块后再回复一次：

```json
{ "type": "balance", "address": "neuro:…", "balance": 50000000, "balance_human": "50", "nonce": 0 }
```

socket 关闭时订阅即结束。

## 通过 socket 调用 RPC

```js
ws.send(JSON.stringify({ type: 'rpc', id: 1, method: 'get_account', params: { address: 'neuro:…' } }))
// → { "type": "rpc_reply", "id": 1, "result": { … }, "error": null }
```

这些方法与 REST 端点一一对应，返回相同的数据：

| 分组 | 方法 |
|-------|---------|
| 链 | `get_status`、`get_blocks`、`get_block`、`get_block_txs`、`get_txs`、`search_tx`、`get_account` |
| 验证者 | `get_validator`、`get_validators` |
| 名称与 NFT | `human_name_price`、`register_human`、`get_my_nfts`、`get_nft`、`transfer_nft` |
| 治理 | `get_governance_params`、`get_proposals`、`get_proposal`、`create_proposal`、`cast_vote` |
| 保险库 | `get_vault_config`、`list_member_vaults`、`get_vault_proposals`、`create_vault_proposal`、`sign_vault_proposal`、`add_vault_member`、`set_vault_threshold` |
| AI | `get_oracle_log`、`get_oracle_entry`、`get_model_registry`、`get_model_registry_entry`、`register_model` |
| 合约 | `get_contracts`、`get_contract`、`get_contract_events`、`deploy_contract`、`call_contract`、`update_contract_score`、`get_certificates`、`get_certificate`、`get_contract_certificate`、`certify_contract` |
| 测试网络 | `faucet` |

写方法需要在 `params` 中携带与其 REST 端点相同的签名字段（`signature`、`public_key`、`nonce`）。

## 限制

| 限制 | 默认值 | 变量 | 超出时 |
|-------|---------|----------|-----------|
| 消息大小 | 64 KB | `WS_MAX_MSG_BYTES` | `{"type":"error","error":"message too large"}` |
| 每个连接的余额订阅数 | 50 | `WS_MAX_SUBS_PER_CLIENT` | `subscription limit exceeded` |
| 每个连接每秒的 RPC 调用数 | 30 | `WS_RPC_MAX_RPS` | `rpc rate limit exceeded` |
| 失效连接清理 | 每 30 s | `WS_STALE_CHECK_MS` | — |

## Python

```python
import asyncio, json, websockets

async def listen():
    async with websockets.connect("ws://localhost:3000/ws") as ws:
        await ws.send(json.dumps({"type": "subscribe_balance", "address": "neuro:<15 hex>.human"}))
        async for raw in ws:
            msg = json.loads(raw)
            print(msg["type"], msg.get("data", msg))

asyncio.run(listen())
```
