# JavaScript SDK

> NeuroChain JavaScript SDK——用于读取、签名转账、合约调用、部署和质押的 dApp SDK，以及让外部 dApp 请求用户钱包签名的 Signer SDK。

Source: https://docs.nro.world/zh/build/sdk

仓库中有两个客户端库：

| 库 | 位置 | 适用场景 |
|----|------|----------|
| **dApp SDK**（`NeuroChain`） | `neurochain-web/src/sdk/neurochain.js` | 你的页面持有用户的密钥（或你在针对自己的钱包编写脚本） |
| **Signer SDK**（`NeuroSigner`） | `packages/neurochain-signer-sdk` | 你的 dApp 运行在其他站点上，用户在弹出窗口中用自己的 NeuroChain 钱包签名 |

两者都尚未发布到 npm；请从仓库复制模块或构建该包。

## dApp SDK

一个没有依赖的 ES 模块。读取使用普通的 `fetch`（浏览器或 Node 18+）；写入使用与钱包相同的加密 WASM 以 ML-DSA-65 签名，因此节点会接受这些签名。

```js
import { NeuroChain, OP_TARGETS, toRaw } from './neurochain.js'

const nc = new NeuroChain({ gateway: 'http://localhost:3000' })
```

| 选项 | 默认值 | 含义 |
|------|--------|------|
| `gateway` | `http://localhost:3000` | gateway 基础 URL |
| `crypto` | — | 已初始化的加密 WASM 模块 |
| `cryptoLoader` | 加载 `../wasm/neuro_wasm_crypto.js` | 返回已初始化模块的异步函数 |

### 金额

你传入的每个 `value` 和 `amount` 都是**人类可读的 NRO**（`'1.5'`、`2`、`'0.000021'`）。SDK 用 `toRaw()` 把它换算为整数 uNRO，因此你签名的金额与发送的金额始终一致。显示时请使用 API 返回的 `*_human` 字段。

### 读取

| 方法 | 调用 | 返回 |
|------|------|------|
| `networkStatus()` | `GET /api/v1/network/status` | 高度、分片、验证者 |
| `shards()` | `GET /api/v1/shards` | 各分片高度 |
| `account(addr)` | `GET /api/v1/accounts/:addr` | `balance`、`balance_human`、`nonce`、… |
| `balance(addr)` | 通过 `account` | `{ raw, human, nonce }` |
| `nextNonce(addr)` | 通过 `account` | 已确认 nonce + 1 |
| `validators()` | `GET /api/v1/validators` | 带质押和指标的验证者列表 |
| `blocks({ limit, offset })` | `GET /api/v1/blocks` | 最新区块 |
| `block(height)` | `GET /api/v1/blocks/:height` | 单个区块 |
| `tx(hash)` | `GET /api/v1/transactions/:hash` | 单笔交易 |
| `contract(addr)` / `contracts({ limit, offset })` | `GET /api/v1/contracts…` | 合约元数据 |
| `gasEstimate()` | `GET /api/v1/gas/estimate` | `base_fee` 和估算值 |
| `contractQuery(addr, fn, args)` | `POST /api/v1/contracts/:addr/call`，带 `dry_run: true` | 解析后的返回值；无交易，不消耗 gas |

### 钱包

```js
const wallet = await nc.walletFromMnemonic('<24 个单词>')
wallet.address    // neuro:<15 hex>.human
wallet.publicKey  // ML-DSA-65 公钥，hex
```

私有 seed 保存在钱包对象内部，绝不会作为属性暴露。

### 写入

```js
// 转账
await nc.transfer({ wallet, to: 'neuro:<地址>.human', value: '1.5', gasPrice })

// 任意交易，可选携带调用数据
await nc.sendTransaction({ wallet, to, value, data, gasPrice })

// 合约调用——JSON 调用数据 { function, args }
await nc.contractCall({ wallet, address: 'neuro:<10 hex>.app', fn: 'transfer', args: { … }, value, gasPrice })

// 部署——发往 neuro:system.deploy 的交易
await nc.deployContract({ wallet, bytecodeHex, name, description, template, initArgs, gasPrice })
```

每次写入都会读取下一个 nonce，对 `nc-tx-v2|from|to|value|nonce` 签名，并发送到 `POST /api/v1/transactions`。

::: warning 设置 gas 价格
SDK 默认 `gasPrice: 1`（部署：5）。节点会拒绝价格低于当前基础费用的交易，而基础费用会随负载上升。请先读取它，再显式传入 `gasPrice`：

```js
const { base_fee } = await nc.gasEstimate()
const gasPrice = Math.ceil(base_fee * 1.5)
```
:::

### 签名操作

质押、名称注册和其他系统操作针对系统目标签名，nonce 为毫秒级时间戳（参见[钱包 → 签名操作](/zh/learn/wallet#signed-operations)）：

```js
const op = nc.signOp(wallet, OP_TARGETS.name, 0)   // { signature, public_key, nonce }
// 把 op 合并进端点的请求体
```

`OP_TARGETS` 包含 `stake`（`neuro:system.stake`）和 `name`（`neuro:system.name`）。

### 已知问题 {#known-issues}

::: danger 当前 SDK 的已知问题
- **非零金额的转账会失败。** `sendTransaction` / `transfer` 把 `value` 作为 JSON 字符串发送；节点只读取数字，并以 `Missing from/to/value` 拒绝请求。金额为 0 的调用和部署不受影响。变通办法——用数字值签名并发送：
  ```js
  const nonce = await nc.nextNonce(wallet.address)
  const value = Number(toRaw('1.5'))               // uNRO，数字类型
  const sig = wallet.sign(to, value, nonce)
  await fetch(GATEWAY + '/api/v1/transactions', { method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ from: wallet.address, to, value, nonce, gas_price, ...sig }) })
  ```
- 当合约的回复不是干净的 JSON 时，`contractQuery` 返回原始字符串——NRC-20 的回复以 NUL 字节结尾，因此你会得到字符串 `'{"ok":true,"value":…}'`；去掉 `\u0000` 后再解析。
- `stake()` 把金额换算为 uNRO 两次后才签名，因此节点会拒绝该签名。在修复之前，请自行构造请求：`{ address, amount: toRaw(amount), ...nc.signOp(wallet, OP_TARGETS.stake, amount) }`，发送到 `/api/v1/staking/stake`。
- `staking(addr)` 请求的是 `/api/v1/staking/:addr`，gateway 并不提供该路径；请使用 `GET /api/v1/staking/info/:address`。
- `contractCall` 会在调用 JSON 中添加 `caller` 字段。合约会忽略它——身份来自宿主——因此无害。
:::

### 加密模块 {#crypto-module}

签名需要 `neuro_wasm_crypto`——钱包的 WASM 构建（`neurochain-web/src/wasm/neuro_wasm_crypto.js` 和 `neuro_wasm_crypto_bg.wasm`）。它必须导出 `nc_keypair_from_words`、`nc_tx_message` 和 `nc_sign`。如果你的页面从其他位置提供它，请传入加载器：

```js
const nc = new NeuroChain({
  gateway: 'http://localhost:3000',
  cryptoLoader: async () => {
    const mod = await import('/wasm/neuro_wasm_crypto.js')
    await mod.default()
    return mod
  },
})
```

没有该模块时，读取照常可用，而每次写入都会抛出异常。

## Signer SDK {#signer-sdk}

适用于位于其他源（交易所、游戏）、且不得看到用户助记词的 dApp。用户的 NeuroChain Web 应用在弹出窗口中打开，用户批准后，签名通过 `postMessage` 返回。

```ts
import { NeuroSigner } from 'neurochain-signer-sdk'

const signer = new NeuroSigner({
  signerUrl: 'http://localhost:8080/signer',      // 钱包应用的 /signer 页面
  allowedOrigins: ['http://localhost:8080'],
})

// 必须直接在点击处理函数中运行，否则浏览器会拦截弹出窗口
button.onclick = async () => {
  const { address, publicKey } = await signer.connect()
  const { signature, nonce } = await signer.signOperation({
    to: 'neuro:system.dex',
    value: 1000000,          // uNRO
  })
}
```

| 方法 | 返回 |
|------|------|
| `connect()` | `{ address, publicKey }` |
| `signOperation({ to, value, actor? })` | `{ signature, publicKey, nonce }`——针对 `actor`（默认：已连接的地址）的签名操作 |
| `isConnected()`、`getAddress()`、`getPublicKey()` | 连接状态 |
| `close()` | 关闭弹出窗口并断开连接 |

错误带有 `code`：`POPUP_BLOCKED`、`CONNECTION_TIMEOUT`、`SIGNATURE_REJECTED`、`INVALID_ORIGIN`、`NOT_CONNECTED`。遇到 `POPUP_BLOCKED` 时，向用户显示签名页面的直接链接。

只接受来自 `allowedOrigins` 的消息；请把它设置为你的钱包应用的源。
