@nmsci/sdk
v4.0.0
Published
NMSCI (Numerical Measurement System for Consumption Intention) SDK — TypeScript library for interacting with the NMSCI REST API
Maintainers
Readme
NMSCI SDK
消费意愿数值化衡量系统(Numerical Measurement System for Consumption Intention, NMSCI)TypeScript/JS SDK。
提供与 NMSCI 后端 REST API 交互的完整能力,包括消息构造、签名、PoW 挖矿、区块链查询等功能。
参考文档:后端 API 参考见 NMSCI 仓库的
docs/API.md;协议字节布局见PROTOCOL.md。
目录
安装
npm install @nmsci/sdk或使用 yarn / pnpm:
yarn add @nmsci/sdk
pnpm add @nmsci/sdk运行时支持:Node.js >= 18,或任何现代浏览器环境。Node 18 会在缺少全局 Web Crypto 时回退到
node:crypto.webcrypto。 开发/测试工具链:Node.js 20.19+ 或 22.12+。Vitest/Vite 需要更高 Node 版本;Node 18 仅执行构建后 pack 运行时冒烟测试。
快速开始
import { ApiClient } from '@nmsci/sdk';
const client = new ApiClient({ baseUrl: 'http://localhost:8080' });
// 查询最新区块
const { data: block } = await client.get('/blocks/latest');
console.log('Latest block height:', block.height);
// 发送原始字节数据
const res = await client.postBinary('/flow-node-registrations', bytes);核心概念
系统角色
| 角色 | 说明 | |------|------| | 流转节点 | 商家/个人的账号节点,参与消费流转,需注册 | | 消费节点 | 发起交易的节点,无需注册 | | 中心节点 | 系统核心节点,负责信息公证与区块固定 |
协议规范
- 签名算法:ECDSA (secp256k1)
- 哈希算法:dblsha256(BTC风格:SHA-256 × 2)
- 字节序:大端序(Big-Endian)
- 时间戳单位:微秒(μs),时区 UTC+0
- 签名格式:Low-S(强制)
消息类型
| 值 | 名称 | 协议完整字节数 | 客户端提交字节数 | |----|------|---:|---:| | 0 | 流转节点注册信息 | 123 | 123 | | 1 | 中心公钥公证信息 | 220 | 148 | | 2 | 中心公钥冻结信息 | 187 | 115 | | 3 | 流转节点冻结信息 | 220 | 148 | | 4 | 交易记录信息 | 335 | 263 | | 5 | 交易挂载信息 | 341 | 269 |
协议完整消息包含 confirmTimestamp 和 centralSignature,用于后端存储、区块固定与 txid 计算;rawBytes 是后端内部缓存,不会在 HTTP 响应中输出。客户端 POST 消息资源时发送的是提交载荷,后端会补齐中心时间戳和中心签名;因此交易记录的完整消息是 335 字节,但发送给当前后端的是 263 字节。
货币类型
| 值 | 名称 | 说明 | |----|------|------| | 0 | 黄金 | 微克(μg) | | 1 | 人民币 | 分 |
API 客户端
配置
import { ApiClient, type SdkConfig } from '@nmsci/sdk';
const config: SdkConfig = {
baseUrl: 'http://localhost:8080', // 后端地址
authToken: 'xxx', // 可选:裸 JWT Token,SDK 自动添加 Bearer
timeout: 15000, // 可选:请求超时(毫秒),默认 15000
};
const client = new ApiClient(config);认证
client.setAuthToken('new-jwt-token');
client.clearAuthToken();HTTP 方法
所有方法返回 Promise<ApiResponse<T>>,结构如下:
interface ApiResponse<T = unknown> {
code: number; // 200 = 成功
message: string; // 响应信息
data: T; // 实际数据
}
// GET 请求
const res = await client.get<T>('/path', { param: 'value' });
if (res.code !== 200) throw new Error(res.message);
console.log(res.data);
// POST 请求
const res = await client.post<T>('/path', bodyData);Raw 静态资源下载
/dat/** 和 /source-code/** 由后端直接返回文件内容,不包裹 ResponseResult<T>。这类接口应使用 raw 方法,SDK 不会尝试按 JSON 解析响应体。
const response = await client.getRaw('/dat/blk00000001.dat');
const contentType = response.headers.get('content-type');
const bytes = await response.arrayBuffer();
const sourceArchive = await client.download('/source-code/source_code_v1.zip');组合入口同样可以通过底层 client 访问:
import { NmsciSdk } from '@nmsci/sdk';
const sdk = new NmsciSdk({ baseUrl: 'http://localhost:8080' });
const datBytes = await sdk.client.download('/dat/blk00000001.dat');Raw 与 Normalized DTO
后端 JSON 中的 amount、confirmTimestamp、height 等 64 位整数以 number 返回,可能超过 Number.MAX_SAFE_INTEGER。SDK 的函数式 API 保持 wire JSON 形态并返回 *Raw 类型;如需 bigint,可显式调用 normalize 函数。ReturningFlowRateResponseDTO 的金额指标是后端 double,normalize 后仍保持 number。
import {
getTransactionRecordMsgById,
normalizeApiResponse,
normalizeTransactionRecordMsg,
} from '@nmsci/sdk';
const raw = await getTransactionRecordMsgById(client, id); // ApiResponse<TransactionRecordMsgRaw>
const normalized = normalizeApiResponse(raw, normalizeTransactionRecordMsg); // ApiResponse<TransactionRecordMsg>
// normalized.data.amount 与 normalized.data.confirmTimestamp 均为 bigint如果需要规范化分页查询结果,可使用 normalizeApiResponseSlice(response, normalizeItem);冻结状态查询的 locked/lockedMsg 包装可使用 normalizeLockedMessageResponseDTO(raw, normalizeItem)。如果原始 number 已超过安全整数范围,normalize 会抛错,避免把已经丢精度的值静默转换成 bigint。difficulty target 保持 hex string,与后端序列化保持一致。
如果希望避免每次手动组合 normalize helper,可使用 NmsciSdk.normalized.*。它保留 ApiResponse<T> envelope,但 data 已经是规范化 DTO:
import { NmsciSdk } from '@nmsci/sdk';
const sdk = new NmsciSdk({ baseUrl: 'http://localhost:8080' });
const block = await sdk.normalized.block.getLast();
const records = await sdk.normalized.transactionRecord.search(undefined, { page: 0, size: 20 });
const height = block.data.height; // bigint
const amount = records.data.content[0]?.amount; // bigint | undefined原始 sdk.* 分组仍返回后端 wire JSON 类型,适合需要完全贴合后端响应的调用方。
组合型 SDK
除了函数式 API,也可以使用 NmsciSdk 组合入口,避免每次手动传入 client:
import { NmsciSdk } from '@nmsci/sdk';
const sdk = new NmsciSdk({ baseUrl: 'http://localhost:8080' });
await sdk.flowNodeRegister.send(submitPayload);
await sdk.flowNodeRegister.list({ page: 0, size: 50 });
await sdk.centralPubkeyLocked.list({ page: 0, size: 50 });
await sdk.flowNode.getState(flowNodePubkey);
await sdk.transactionRecord.getByFlowNodePubkey(flowNodePubkey, { page: 0, size: 50 });
await sdk.returningFlowRate.getByPubkey({ targetPubkey: flowNodePubkey, currencyType: 1 });
await sdk.verify.chain({ stateful: false });
await sdk.actuator.health();也可以按子路径导入:
import { ApiClient } from '@nmsci/sdk/api';
import { serializeTransactionRecordSubmitPayload } from '@nmsci/sdk/messages';
import { MSG_SPECS } from '@nmsci/sdk/protocol';Breaking changes / migration
queryConsumeChainsnow requires one query selector: exactly one ofstartId/endId/nodeId/startPubkey/endPubkey/nodePubkey, ormountedTransactionId.
本版本跟随后端 API 合同调整,建议按 major 版本发布:
sendCentralPubkeyLockedMsg从Promise<void>改为Promise<ApiResponse<CentralPubkeyLockedMsgRaw>>,成功后可读取后端补齐的落库实体。getConsumeChainEdges从ApiResponse<ConsumeChainEdgeRaw[]>改为ApiResponse<SliceResponseDTO<ConsumeChainEdgeRaw>>。迁移时把response.data.map(...)改为response.data.content.map(...),并根据hasNext翻页。BlockInfoRaw和 6 类协议消息 Raw DTO 不再包含rawBytes字段;后端 HTTP 响应不输出该内部缓存字段。
核心工具模块
密钥生成
import { generateKeyPair, getPublicKeyFromPrivate, validatePublicKey, validatePrivateKey } from '@nmsci/sdk';
// 生成随机密钥对
const { privateKey, publicKey } = generateKeyPair();
// privateKey: 64字符十六进制字符串(32字节)
// publicKey: 66字符十六进制字符串(33字节压缩公钥)
// 从私钥推导公钥
const pubkey = getPublicKeyFromPrivate(privateKey);
// 验证公钥合法性
const result = validatePublicKey(publicKey);
if (!result.isValid) console.error(result.error);
// 验证私钥合法性
if (!validatePrivateKey(privateKey)) throw new Error('Invalid private key');签名与验签
import { signData, verifySignature } from '@nmsci/sdk';
const data = new Uint8Array([0x00, 0x01, 0x02]);
// 签名(自动 Low-S 规范化)
const signatureBytes = await signData(data, privateKeyHex);
const signatureHex = Array.from(signatureBytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
// 验签
const isValid = await verifySignature(data, signatureBytes, publicKeyHex);字节编码
import { toBytesBigEndian, fromHex, toHex, concat, uuidToBytes, bytesToUuid } from '@nmsci/sdk';
// 整数 → 大端字节
const u8 = toBytesBigEndian(65535, 2); // Uint8Array(2)
const u32 = toBytesBigEndian(0x12345678, 4);
const u64 = toBytesBigEndian(1_000_000n, 8);
// 十六进制 ↔ 字节
const bytes = fromHex('02aabbcc'); // Hex → Uint8Array
const hex = toHex(new Uint8Array([1,2,3])); // Uint8Array → Hex
// UUID 互转
const uuidBytes = uuidToBytes('550e8400-e29b-41d4-a716-446655440000');
const uuid = bytesToUuid(uuidBytes);
// 字节拼接
const combined = concat(u8, u32, bytes);工作量证明(PoW)
import { mineNonce, calculateTargetFromNBits, doubleSha256Hex, compareHex } from '@nmsci/sdk';
// 从 nBits(紧凑格式)计算难度目标
const targetHex = calculateTargetFromNBits('0x1effffff'); // "0000ffff..."
// 挖矿:寻找满足 dblsha256(prefix + nonce + suffix) < target 的 nonce
const nonce = await mineNonce(
prefix, // nonce 之前的字节
suffix, // nonce 之后的字节
targetHex,
(attempts, hash, nonce) => {
console.log(`Tried ${attempts}, current hash: ${hash}`);
}
);
console.log('Found valid nonce:', nonce);
// 直接计算双 SHA-256
const hash = await doubleSha256Hex(data);
// 比较两个十六进制字符串(按大端整数比较)
compareHex('0000ffff...', '00010000...'); // -1消息序列化
每个消息类型都有两类序列化函数:serializeXxxSubmitPayload 生成客户端 POST /send 使用的提交载荷,serializeXxxFullMessage 生成包含中心确认字段的协议完整消息。旧的 serializeXxx 仍保留为完整消息兼容别名。
关键:所有签名均基于特定的预签名载荷(不含签名字段的字节序列)计算,具体字段分布见各消息说明。
流转节点注册(消息类型 0,123 字节)
import {
MsgType,
buildFlowNodeRegisterPayload,
serializeFlowNodeRegister,
signData,
} from '@nmsci/sdk';
import { toBytesBigEndian, concat, fromHex, toHex } from '@nmsci/sdk';
// 1. 构造注册信息
const msg = {
msgType: MsgType.FLOW_NODE_REGISTRATION,
uuid: '550e8400e29b41d4a716446655440000', // 32字符无破折号
registerDifficultyTarget: '0x1effffff',
nonce: 12345, // 从 PoW 挖矿得到
flowNodePubkey: '02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
flowNodeSignature: undefined, // 待签名后填入
};
// 2. 计算预签名载荷(59字节 = 2+16+4+4+33)
const payload = buildFlowNodeRegisterPayload({
uuid: msg.uuid,
registerDifficultyTarget: msg.registerDifficultyTarget,
nonce: msg.nonce,
flowNodePubkey: msg.flowNodePubkey,
});
// 3. 签名(dblsha256(data) 后用私钥签名)
const sig = await signData(payload, flowNodePrivateKey);
msg.flowNodeSignature = toHex(sig) as Signature;
// 4. 序列化为 123 字节
const bytes = serializeFlowNodeRegister(msg);
// 5. 提交(ArrayBuffer → number[])
const res = await client.postBinary('/flow-node-registrations', bytes);字段布局(123 字节):
| 字段 | 字节数 | 说明 |
|------|--------|------|
| msgType | 2 | 固定 0x0000 |
| uuid | 16 | 信息唯一标识 |
| registerDifficultyTarget | 4 | 注册难度目标(大端序十六进制) |
| nonce | 4 | PoW 随机数(大端序) |
| flowNodePubkey | 33 | 流转节点压缩公钥 |
| flowNodeSignature | 64 | 流转节点对前5项数据的签名 |
中心公钥授权(消息类型 1,完整 220 字节,提交 148 字节)
import { MsgType, buildCentralPubkeyEmpowerPayload, buildCentralPubkeyEmpowerFullPayload, serializeCentralPubkeyEmpowerSubmitPayload, serializeCentralPubkeyEmpowerFullMessage, signData } from '@nmsci/sdk';
import { fromHex, toHex } from '@nmsci/sdk';
const msg = {
msgType: MsgType.CENTRAL_KEY_AUTH,
uuid: '660e8400e29b41d4a716446655440001',
flowNodePubkey: '02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
centralPubkey: '03bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
flowNodeSignature: undefined,
confirmTimestamp: BigInt(Date.now()) * 1000n, // 微秒时间戳
centralSignature: undefined,
};
// 第一步:流转节点签名(签 preSigPayload = 84字节)
const prePayload = buildCentralPubkeyEmpowerPayload({
uuid: msg.uuid,
flowNodePubkey: msg.flowNodePubkey,
centralPubkey: msg.centralPubkey,
});
msg.flowNodeSignature = toHex(await signData(prePayload, flowNodePrivateKey)) as Signature;
// 后端会生成 confirmTimestamp 和 centralSignature;离线完整消息校验时中心签名对象为 156 字节
const fullPayload = buildCentralPubkeyEmpowerFullPayload({
uuid: msg.uuid,
flowNodePubkey: msg.flowNodePubkey,
centralPubkey: msg.centralPubkey,
flowNodeSignature: msg.flowNodeSignature,
confirmTimestamp: msg.confirmTimestamp,
});
msg.centralSignature = toHex(await signData(fullPayload, centralPrivateKey)) as Signature;
await client.postBinary('/central-pubkey-empowerments', serializeCentralPubkeyEmpowerSubmitPayload(msg));
const fullBytes = serializeCentralPubkeyEmpowerFullMessage(msg); // 220 字节,用于本地校验完整消息字节/txid中心公钥冻结(消息类型 2,完整 187 字节,提交 115 字节)
import { MsgType, buildCentralPubkeyLockedPayload, buildCentralPubkeyLockedFullPayload, serializeCentralPubkeyLockedSubmitPayload, serializeCentralPubkeyLockedFullMessage, signData } from '@nmsci/sdk';
const msg = {
msgType: MsgType.CENTRAL_KEY_FREEZE,
uuid: '770e8400e29b41d4a716446655440002',
centralPubkey: '03bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
centralSignaturePre: undefined,
confirmTimestamp: BigInt(Date.now()) * 1000n,
centralSignature: undefined,
};
// 预签名(51字节):msgType + uuid + centralPubkey
const prePayload = buildCentralPubkeyLockedPayload({
uuid: msg.uuid,
centralPubkey: msg.centralPubkey,
});
msg.centralSignaturePre = toHex(await signData(prePayload, centralPrivateKey)) as Signature;
// 离线完整消息校验时中心签名对象为 123 字节
const fullPayload = buildCentralPubkeyLockedFullPayload({
uuid: msg.uuid,
centralPubkey: msg.centralPubkey,
centralSignaturePre: msg.centralSignaturePre,
confirmTimestamp: msg.confirmTimestamp,
});
msg.centralSignature = toHex(await signData(fullPayload, centralPrivateKey)) as Signature;
const res = await client.postBinary('/central-pubkey-locks', serializeCentralPubkeyLockedSubmitPayload(msg));
const fullBytes = serializeCentralPubkeyLockedFullMessage(msg); // 187 字节,用于本地校验完整消息字节/txid
// res.data 为后端补齐 confirmTimestamp/centralSignature 后的落库实体流转节点冻结(消息类型 3,完整 220 字节,提交 148 字节)
与中心公钥授权流程类似,需要流转节点和中心节点双重签名。
import { MsgType, buildFlowNodeLockedPayload, buildFlowNodeLockedFullPayload, serializeFlowNodeLockedSubmitPayload, serializeFlowNodeLockedFullMessage, signData } from '@nmsci/sdk';
const msg = {
msgType: MsgType.FLOW_NODE_FREEZE,
uuid: '880e8400e29b41d4a716446655440003',
flowNodePubkey: '02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
centralPubkey: '03bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
flowNodeSignature: undefined,
confirmTimestamp: BigInt(Date.now()) * 1000n,
centralSignature: undefined,
};
// 流转节点签名(84字节)
const prePayload = buildFlowNodeLockedPayload({
uuid: msg.uuid,
flowNodePubkey: msg.flowNodePubkey,
centralPubkey: msg.centralPubkey,
});
msg.flowNodeSignature = toHex(await signData(prePayload, flowNodePrivateKey)) as Signature;
// 离线完整消息校验时中心签名对象为 156 字节
const fullPayload = buildFlowNodeLockedFullPayload({
...msg,
flowNodeSignature: msg.flowNodeSignature,
confirmTimestamp: msg.confirmTimestamp,
});
msg.centralSignature = toHex(await signData(fullPayload, centralPrivateKey)) as Signature;
await client.postBinary('/flow-node-locks', serializeFlowNodeLockedSubmitPayload(msg));
const fullBytes = serializeFlowNodeLockedFullMessage(msg); // 220 字节,用于本地校验完整消息字节/txid交易记录(消息类型 4,完整 335 字节,提交 263 字节)
需要 PoW + 消费节点签名 + 流转节点签名 + 中心节点签名。
import {
MsgType, CurrencyType,
buildTransactionRecordPayload, buildTransactionRecordFullPayload,
serializeTransactionRecordSubmitPayload, serializeTransactionRecordFullMessage,
signTransactionRecordPayload, mineTransactionRecordNonce,
} from '@nmsci/sdk';
import { fromHex, toHex, toBytesBigEndian, concat } from '@nmsci/sdk';
// 构造 noncePrefix(32字节)和 nonceSuffix(99字节)
const uid = new Uint8Array(16);
crypto.getRandomValues(uid);
const uuid = toHex(uid); // 32字符
const noncePrefix = concat(
toBytesBigEndian(MsgType.TRANSACTION_RECORD, 2), // 2 bytes msgType
uid, // 16 bytes uuid
toBytesBigEndian(10000n, 8), // 8 bytes amount(分 or 微克)
toBytesBigEndian(CurrencyType.RMB_CENT, 2), // 2 bytes currencyType
fromHex('0x1effffff'.padStart(8, '0')), // 4 bytes difficultyTarget
); // 共 32 字节
const nonceSuffix = concat(
fromHex(consumeNodePubkey), // 33 bytes
fromHex(flowNodePubkey), // 33 bytes
fromHex(centralPubkey), // 33 bytes
); // 共 99 字节
// PoW 挖矿
const nonce = await mineTransactionRecordNonce(
noncePrefix,
nonceSuffix,
'0x1effffff',
(attempts, hash, n) => {
if (attempts % 10000 === 0) console.log(`Mining: ${attempts} attempts...`);
}
);
console.log('PoW nonce found:', nonce);
// 构建完整 135 字节载荷
const payload = buildTransactionRecordPayload({
uuid,
amount: 10000n,
currencyType: CurrencyType.RMB_CENT,
transactionDifficultyTarget: '0x1effffff',
nonce,
consumeNodePubkey,
flowNodePubkey,
centralPubkey,
});
// 消费节点签名
const consumeSig = await signTransactionRecordPayload(payload, consumeNodePrivateKey);
// 流转节点签名
const flowSig = await signTransactionRecordPayload(payload, flowNodePrivateKey);
// 离线完整消息校验时中心签名对象为 271 字节
const fullPayload = buildTransactionRecordFullPayload({
uuid, amount: 10000n, currencyType: CurrencyType.RMB_CENT,
transactionDifficultyTarget: '0x1effffff', nonce,
consumeNodePubkey, flowNodePubkey, centralPubkey,
consumeNodeSignature: consumeSig,
flowNodeSignature: flowSig,
confirmTimestamp: BigInt(Date.now()) * 1000n,
});
const centralSig = await signData(fullPayload, centralPrivateKey);
// 组装消息
const msg = {
msgType: MsgType.TRANSACTION_RECORD,
uuid, amount: 10000n, currencyType: CurrencyType.RMB_CENT,
transactionDifficultyTarget: '0x1effffff', nonce,
consumeNodePubkey, flowNodePubkey, centralPubkey,
consumeNodeSignature: consumeSig,
flowNodeSignature: flowSig,
confirmTimestamp: BigInt(Date.now()) * 1000n,
centralSignature: toHex(centralSig) as Signature,
};
await client.postBinary('/transaction-records', serializeTransactionRecordSubmitPayload(msg));
const fullBytes = serializeTransactionRecordFullMessage(msg); // 335 字节,用于本地校验完整消息字节/txid交易挂载(消息类型 5,完整 341 字节,提交 269 字节)
与交易记录类似,但引用一笔已存在的交易记录 ID 作为起点。
import {
MsgType,
buildTransactionMountPayload, buildTransactionMountFullPayload,
serializeTransactionMountSubmitPayload, serializeTransactionMountFullMessage,
signTransactionMountPayload, mineTransactionMountNonce,
} from '@nmsci/sdk';
import { toBytesBigEndian, concat, fromHex, toHex } from '@nmsci/sdk';
// 被挂载的交易记录 ID
const mountedRecordId = '11223344556677889900112233445566';
// 构造 noncePrefix(38字节)和 nonceSuffix(99字节)
const uid = new Uint8Array(16);
crypto.getRandomValues(uid);
const uuid = toHex(uid);
const noncePrefix = concat(
toBytesBigEndian(MsgType.TRANSACTION_MOUNT, 2), // 2 bytes
uid, // 16 bytes
fromHex(mountedRecordId), // 16 bytes
fromHex('0x1effffff'.padStart(8, '0')), // 4 bytes
); // 共 38 字节
const nonceSuffix = concat(
fromHex(consumeNodePubkey),
fromHex(flowNodePubkey),
fromHex(centralPubkey),
); // 共 99 字节
// PoW 挖矿
const nonce = await mineTransactionMountNonce(noncePrefix, nonceSuffix, '0x1effffff');
// 构建 141 字节载荷并签名
const payload = buildTransactionMountPayload({
uuid, mountedTransactionRecordId: mountedRecordId,
transactionDifficultyTarget: '0x1effffff', nonce,
consumeNodePubkey, flowNodePubkey, centralPubkey,
});
const consumeSig = await signTransactionMountPayload(payload, consumeNodePrivateKey);
const flowSig = await signTransactionMountPayload(payload, flowNodePrivateKey);
// 离线完整消息校验时中心签名对象为 277 字节
const fullPayload = buildTransactionMountFullPayload({
uuid, mountedTransactionRecordId: mountedRecordId,
transactionDifficultyTarget: '0x1effffff', nonce,
consumeNodePubkey, flowNodePubkey, centralPubkey,
consumeNodeSignature: consumeSig, flowNodeSignature: flowSig,
confirmTimestamp: BigInt(Date.now()) * 1000n,
});
const centralSig = await signData(fullPayload, centralPrivateKey);
const msg = {
msgType: MsgType.TRANSACTION_MOUNT,
uuid, mountedTransactionRecordId: mountedRecordId,
transactionDifficultyTarget: '0x1effffff', nonce,
consumeNodePubkey, flowNodePubkey, centralPubkey,
consumeNodeSignature: consumeSig,
flowNodeSignature: flowSig,
confirmTimestamp: BigInt(Date.now()) * 1000n,
centralSignature: toHex(centralSig) as Signature,
};
await client.postBinary('/transaction-mounts', serializeTransactionMountSubmitPayload(msg));
const fullBytes = serializeTransactionMountFullMessage(msg); // 341 字节,用于本地校验完整消息字节/txidAPI 接口一览
所有接口均通过 ApiClient 实例调用。通用响应格式:
{ code: 200, message: 'Success', data: T }流转节点状态
// 按流转节点公钥查询注册/授权/冻结状态
getFlowNodeState(client, flowNodePubkey: string): Promise<ApiResponse<FlowNodeStateResponseDTO>>
// 流转节点列表(分页,可按 registered/authorized/locked 过滤)
listFlowNodes(client, query?: { registered?: boolean; authorized?: boolean; locked?: boolean } & PageQuery): Promise<ApiResponse<SliceResponseDTO<FlowNodeListItemDTORaw>>>流转节点注册
// 发送注册信息
sendFlowNodeRegisterMsg(client, byteArray: number[]): Promise<ApiResponse<FlowNodeRegisterMsgRaw>>
// 按 UUID 查询
getFlowNodeRegisterMsgById(client, id: string): Promise<ApiResponse<FlowNodeRegisterMsgRaw>>
// 按流转节点公钥查询(集合根,返回分页 Slice)
getFlowNodeRegisterMsgByFlowNodePubkey(client, flowNodePubkey: string, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<FlowNodeRegisterMsgRaw>>>
// flowNodePubkey: 66字符十六进制字符串
// 集合根(过滤参数可选;全空返回分页全量)
listFlowNodeRegisterMsgs(client, query?: { flowNodePubkey?: string } & PageQuery): Promise<ApiResponse<SliceResponseDTO<FlowNodeRegisterMsgRaw>>>中心公钥授权
// 发送授权信息
sendCentralPubkeyEmpowerMsg(client, byteArray: number[]): Promise<ApiResponse<CentralPubkeyEmpowerMsgRaw>>
// 按 UUID 查询
getCentralPubkeyEmpowerMsgById(client, id: string): Promise<ApiResponse<CentralPubkeyEmpowerMsgRaw>>
// 按流转节点公钥查询(集合根,返回分页 Slice)
getCentralPubkeyEmpowerMsgByFlowNodePubkey(client, flowNodePubkey: string, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<CentralPubkeyEmpowerMsgRaw>>>
// 集合根(过滤参数可选;全空返回分页全量)
listCentralPubkeyEmpowerMsgs(client, query?: { flowNodePubkey?: string } & PageQuery): Promise<ApiResponse<SliceResponseDTO<CentralPubkeyEmpowerMsgRaw>>>中心公钥冻结
// 发送冻结信息
sendCentralPubkeyLockedMsg(client, byteArray: number[]): Promise<ApiResponse<CentralPubkeyLockedMsgRaw>>
// 按 UUID 查询
getCentralPubkeyLockedMsgById(client, id: string): Promise<ApiResponse<CentralPubkeyLockedMsgRaw>>
// 集合根(仅分页)
listCentralPubkeyLockedMsgs(client, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<CentralPubkeyLockedMsgRaw>>>
// 按中心公钥查询
getCentralPubkeyLockedMsgByCentralPubkey(client, centralPubkey: string): Promise<ApiResponse<LockedMessageResponseDTO<CentralPubkeyLockedMsgRaw>>>流转节点冻结
// 发送冻结信息
sendFlowNodeLockedMsg(client, byteArray: number[]): Promise<ApiResponse<FlowNodeLockedMsgRaw>>
// 按 UUID 查询
getFlowNodeLockedMsgById(client, id: string): Promise<ApiResponse<FlowNodeLockedMsgRaw>>
// 集合根(仅分页)
listFlowNodeLockedMsgs(client, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<FlowNodeLockedMsgRaw>>>
// 按流转节点公钥查询
getFlowNodeLockedMsgByFlowNodePubkey(client, flowNodePubkey: string): Promise<ApiResponse<LockedMessageResponseDTO<FlowNodeLockedMsgRaw>>>交易记录
// 发送交易记录
sendTransactionRecordMsg(client, byteArray: number[]): Promise<ApiResponse<TransactionRecordMsgRaw>>
// 按 UUID 查询
getTransactionRecordMsgById(client, id: string): Promise<ApiResponse<TransactionRecordMsgRaw>>
// 按消费节点公钥查询(返回分页 Slice)
getTransactionRecordMsgByConsumeNodePubkey(client, consumeNodePubkey: string, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<TransactionRecordMsgRaw>>>
// 按流转节点公钥查询(返回分页 Slice)
getTransactionRecordMsgByFlowNodePubkey(client, flowNodePubkey: string, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<TransactionRecordMsgRaw>>>
// 按双方公钥查询(返回分页 Slice)
getTransactionRecordMsgByBothPubkeys(client, consumeNodePubkey: string, flowNodePubkey: string, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<TransactionRecordMsgRaw>>>
// 通用检索集合根(过滤参数全部可选;currencyType/startTime/endTime,时间为微秒)
searchTransactionRecordMsgs(client, filters?: { consumeNodePubkey?: string; flowNodePubkey?: string; currencyType?: number; startTime?: number; endTime?: number }, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<TransactionRecordMsgRaw>>>交易挂载
// 发送交易挂载
sendTransactionMountMsg(client, byteArray: number[]): Promise<ApiResponse<TransactionMountMsgRaw>>
// 按 UUID 查询
getTransactionMountMsgById(client, id: string): Promise<ApiResponse<TransactionMountMsgRaw>>
// 按被挂载的交易记录 ID 查询(集合根,返回分页 Slice;命中为空返回空集合 + 200,而非旧版 404)
getTransactionMountMsgByMountedTransactionRecordId(client, id: string, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<TransactionMountMsgRaw>>>
// 按消费节点公钥查询(返回分页 Slice)
getTransactionMountMsgByConsumeNodePubkey(client, consumeNodePubkey: string, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<TransactionMountMsgRaw>>>
// 按流转节点公钥查询(返回分页 Slice)
getTransactionMountMsgByFlowNodePubkey(client, flowNodePubkey: string, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<TransactionMountMsgRaw>>>
// 按双方公钥查询(返回分页 Slice)
getTransactionMountMsgByBothPubkeys(client, consumeNodePubkey: string, flowNodePubkey: string, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<TransactionMountMsgRaw>>>
// 通用检索集合根(过滤参数全部可选;mountedTransactionRecordId/startTime/endTime,时间为微秒)
searchTransactionMountMsgs(client, filters?: { consumeNodePubkey?: string; flowNodePubkey?: string; mountedTransactionRecordId?: string; startTime?: number; endTime?: number }, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<TransactionMountMsgRaw>>>区块链
// 查询最新区块
getLastBlock(client): Promise<ApiResponse<BlockInfoRaw>>
// 按区块高度查询
getBlockByHeight(client, height: number): Promise<ApiResponse<BlockInfoRaw>>
// 按区块头哈希查询
getBlockByHash(client, hash: string): Promise<ApiResponse<BlockInfoRaw>> // hash: 64字符十六进制字符串消费链
// 按消费链 UUID 查询
getConsumeChainById(client, id: string): Promise<ApiResponse<ConsumeChainResponseDTORaw>>
// 集合根(分页):id 模式(startId/endId/nodeId)与 pubkey 模式(startPubkey/endPubkey/nodePubkey)
// 不可混用,混用后端返回 400
// Requires exactly one selector, or mountedTransactionId.
queryConsumeChains(client, filters: ConsumeChainQueryFilters, pagination?: PaginationQuery): Promise<ApiResponse<SliceResponseDTO<ConsumeChainResponseDTORaw>>>
// 便捷封装(均走集合根;startId/endId/nodeId 为流转节点 UUID)
getConsumeChainByStart(client, startId: string, query?: boolean | ConsumeChainQuery): Promise<ApiResponse<SliceResponseDTO<ConsumeChainResponseDTORaw>>>
getConsumeChainByEnd(client, endId: string, query?: boolean | ConsumeChainQuery): Promise<ApiResponse<SliceResponseDTO<ConsumeChainResponseDTORaw>>>
getConsumeChainByNode(client, nodeId: string, query?: boolean | ConsumeChainQuery): Promise<ApiResponse<SliceResponseDTO<ConsumeChainResponseDTORaw>>>
getConsumeChainByMountedTransaction(client, mountedTransactionId: string, pagination?: PageQuery): Promise<ApiResponse<SliceResponseDTO<ConsumeChainResponseDTORaw>>>
// 边查询(流入某 target 的边集合,返回分页 Slice);id 模式和 pubkey 模式二选一,不可混用
getConsumeChainEdges(client, params: ({
targetId: string;
sourceId?: string;
} | {
targetPubkey: string;
sourcePubkey?: string;
}) & {
currencyType?: number; startTime?: number; endTime?: number; // 时间为微秒
page?: number; size?: number;
}): Promise<ApiResponse<SliceResponseDTO<ConsumeChainEdgeRaw>>>回流率
// 按 UUID 查询
getReturningFlowRateById(client, {
sourceId?, // 可选:源流转节点 UUID(空则查询总滞留指数)
targetId, // 必填:目标流转节点 UUID
startTime?, // 可选:开始时间(微秒),默认 0
endTime?, // 可选:结束时间(微秒),默认 Long.MAX_VALUE
currencyType?, // 可选:0=黄金,1=人民币,默认 1
}): Promise<ApiResponse<ReturningFlowRateResponseDTORaw>>
// 按公钥查询
getReturningFlowRateByPubkey(client, {
sourcePubkey?, // 可选:源流转节点公钥(空则查询总滞留指数)
targetPubkey, // 必填:目标流转节点公钥
startTime?,
endTime?,
currencyType?,
}): Promise<ApiResponse<ReturningFlowRateResponseDTORaw>>
// 返回字段说明
// returningFlowRate = 已成环金额 / 全部消费链金额
// loopedAmount = source→target 已成环金额总和
// unloopedAmount = source→target 未成环金额总和(滞留指数)
// targetTotalLoopedAmount = target 节点所有已成环金额(总滞留指数)
// targetTotalUnloopedAmount = target 节点所有未成环金额(总滞留指数)
// currencyType = 货币类型系统
// 系统参数(版本、中心公钥、难度、源码包哈希、最新区块)
getSystemParams(client): Promise<ApiResponse<SystemParamsDTORaw>>
// 运行状态(最新区块、未入块消息数、最早未确认时间戳、区块间隔毫秒、中心公钥是否冻结)
getSystemStatus(client): Promise<ApiResponse<SystemStatusDTORaw>>
// .dat 存储用量(目录、文件数、当前文件大小、总字节、单文件上限、利用率)
getSystemStorage(client): Promise<ApiResponse<StorageStatusDTORaw>>链验证
// 重新解析本节点落盘 blk*.dat 并独立核验链完整性;stateful 默认 true
verifyChain(client, query?: { stateful?: boolean }): Promise<ApiResponse<ChainVerificationSummaryDTORaw>>
// Actuator endpoints are not wrapped in ApiResponse<ResponseResult<T>>.
getActuatorHealth(client): Promise<ActuatorHealthDTO>
getActuatorInfo(client): Promise<ActuatorInfoDTO>
getActuatorMetrics(client): Promise<ActuatorMetricsDTO>
getActuatorMetric(client, name: string): Promise<ActuatorMetricDTO>
getActuatorPrometheus(client): Promise<string>ChainVerificationSummaryDTORaw 的 messageCount、passedChecks、failedChecks、skippedChecks 是 64 位计数字段;如需 bigint,使用 normalizeChainVerificationSummary 或 sdk.normalized.verify.chain(...)。
元数据
// 消息类型(size=落库字节数,inboundSize=入站 POST 字节数)
getMessageTypes(client): Promise<ApiResponse<MsgTypeMetadataDTO[]>>
// 货币类型
getCurrencyTypes(client): Promise<ApiResponse<CurrencyTypeMetadataDTO[]>>
// 区块/存储格式常量
getBlockFormat(client): Promise<ApiResponse<BlockFormatMetadataDTO>>
// 当前注册/交易 PoW 难度(nbits 与解码后的目标阈值)
getDifficulty(client): Promise<ApiResponse<DifficultyMetadataDTO>>系统/存储的大整数字段(区块高度、微秒时间戳、存储字节等)以 wire JSON
number返回;如需 bigint,使用对应normalizeSystemStatus/normalizeStorageStatus/normalizeSystemParams。
完整使用示例
以下示例演示完整注册一个新流转节点的流程:
import {
ApiClient,
generateKeyPair,
MsgType,
buildFlowNodeRegisterPayload,
serializeFlowNodeRegister,
signData,
calculateTargetFromNBits,
mineNonce,
sendFlowNodeRegisterMsg,
getFlowNodeRegisterMsgByFlowNodePubkey,
toBytesBigEndian,
concat,
fromHex,
toHex,
} from '@nmsci/sdk';
async function registerFlowNode(centralPubkey: string, difficultyTarget: string) {
// 1. 初始化客户端
const client = new ApiClient({ baseUrl: 'http://localhost:8080' });
// 2. 生成流转节点密钥对
const { privateKey: flowNodePrivateKey, publicKey: flowNodePubkey } = generateKeyPair();
console.log('Generated pubkey:', flowNodePubkey);
// 3. 检查是否已注册
const existing = await getFlowNodeRegisterMsgByFlowNodePubkey(client, flowNodePubkey);
if (existing.code === 200) {
console.log('Already registered:', existing.data.id);
return existing.data;
}
// 4. 生成 UUID
const uid = new Uint8Array(16);
crypto.getRandomValues(uid);
const uuid = toHex(uid);
// 5. PoW 挖矿(构造 noncePrefix = 22字节,nonceSuffix = 33字节)
const noncePrefix = concat(
toBytesBigEndian(MsgType.FLOW_NODE_REGISTRATION, 2), // 2 bytes
uid, // 16 bytes
fromHex(difficultyTarget.padStart(8, '0')), // 4 bytes
); // 共 22 字节
const nonceSuffix = fromHex(flowNodePubkey); // 33 字节
const targetHex = calculateTargetFromNBits(difficultyTarget);
console.log('Starting PoW mining...');
const nonce = await mineNonce(noncePrefix, nonceSuffix, targetHex, (att, hash, n) => {
if (att % 5000 === 0) console.log(` attempts=${att}, nonce=${n}`);
});
console.log(`PoW complete: nonce=${nonce}`);
// 6. 构建预签名载荷(59字节)并签名
const payload = buildFlowNodeRegisterPayload({
uuid,
registerDifficultyTarget: difficultyTarget,
nonce,
flowNodePubkey,
});
const sig = await signData(payload, flowNodePrivateKey);
// 7. 组装消息并序列化
const msgBytes = serializeFlowNodeRegister({
msgType: MsgType.FLOW_NODE_REGISTRATION,
uuid,
registerDifficultyTarget: difficultyTarget,
nonce,
flowNodePubkey,
flowNodeSignature: toHex(sig) as any,
});
// 8. 发送到后端
console.log('Submitting registration...');
const res = await sendFlowNodeRegisterMsg(client, Array.from(msgBytes));
if (res.code === 200) {
console.log('Registration successful!');
console.log(' ID:', res.data.id);
console.log(' txid:', res.data.txid);
return res.data;
} else {
throw new Error(`Registration failed: ${res.message}`);
}
}
// 使用
const difficultyTarget = '0x1effffff'; // 从 getLastBlock() 获取
const centralPubkey = '03cccccccccccccccccccccccccccccccccccccccccccccccccccccccccc';
registerFlowNode(centralPubkey, difficultyTarget).catch(console.error);类型速查
// 基础类型
type HexString = string; // 无 0x 前缀的十六进制字符串
type Pubkey = HexString; // 66字符(33字节压缩公钥)
type Signature = HexString; // 128字符(64字节 ECDSA 签名)
type UUID = HexString; // 32字符(16字节 UUID,无破折号)
// 消息类型
enum MsgType {
FLOW_NODE_REGISTRATION = 0,
CENTRAL_KEY_AUTH = 1,
CENTRAL_KEY_FREEZE = 2,
FLOW_NODE_FREEZE = 3,
TRANSACTION_RECORD = 4,
TRANSACTION_MOUNT = 5,
}
// 货币类型
enum CurrencyType {
GOLD_MICROGRAM = 0,
RMB_CENT = 1,
}错误处理
import { ApiClient } from '@nmsci/sdk';
const client = new ApiClient({ baseUrl: 'http://localhost:8080' });
async function safeApiCall<T>(fn: () => Promise<{ code: number; message: string; data: T }>) {
try {
const res = await fn();
if (res.code !== 200) {
throw new Error(`API Error [${res.code}]: ${res.message}`);
}
return res.data;
} catch (e) {
if (e instanceof TypeError && e.message.includes('fetch')) {
throw new Error('Network error: Unable to reach the server');
}
throw e;
}
}
// 使用
const block = await safeApiCall(() => client.get('/blocks/latest'));浏览器兼容性
本 SDK 依赖以下 Web API:
| API | 最低版本 |
|-----|---------|
| crypto.subtle | Chrome 37, Firefox 34, Safari 11, Edge 12 |
| crypto.getRandomValues | 所有现代浏览器 |
如需在旧版浏览器中使用,可引入 crypto polyfill(如 webcrypto-liner);Node.js 环境建议使用 >= 18。
开发与校验
完整本地校验请使用 Node.js 20.19+ 或 22.12+;Node 18 仅执行构建后 pack 运行时冒烟测试,用于确认发布包在运行时可导入和执行。
本地提交前建议按 CI 同序执行:
npm ci
npm run test:encoding
npm run typecheck
npm test
npm run test:types
npm run build
npm run test:pack:preparedtest:encoding 会扫描已跟踪的文本文件,发现 Unicode replacement character 或常见 UTF-8 mojibake 标记时失败。不要仅凭 PowerShell 终端显示判断文件损坏;以 UTF-8 文件内容和该检查结果为准。
test:pack:prepared 假定 dist 已由上一条 npm run build 生成,用于 CI / release 的构建后 pack 冒烟检查,避免 pack 检查自身重复构建。release 脚本在 publish 阶段还会使用 --ignore-scripts 跳过 npm lifecycle scripts,确保最终发布产物仍来自显式 build + pack smoke 路径。单独在本地检查发布包时仍可运行 npm run test:pack,它会先构建再执行 pack 冒烟测试。
发布(维护者)
本包使用 scripts/release.mjs 一键发布。脚本会按顺序执行:环境检查 → 编码检查 → typecheck → 测试 → 类型级测试 → bump 版本 → 构建 → pack 冒烟测试 → npm publish --access public --ignore-scripts → git commit + tag。发布阶段会跳过 npm lifecycle scripts,因为显式 build + pack smoke 已完成;package.json 的 prepublishOnly 仍保留为手动 npm publish 的安全网。git commit/tag 只在 npm publish 成功后才执行;创建版本 commit 之前的失败会逐字节回滚 package.json / package-lock.json 的版本改动,commit/tag/publish 边界上的失败可能需要按 git status 和实际发布状态人工清理。
当前 GitHub Actions 只做验证,不自动发布。若后续启用 npm Trusted Publishing / provenance,需要先在 npm 包侧配置 trusted publisher,再增加带
id-token: write权限的发布 workflow,并使用满足 npm 要求的 Node/npm 版本。
前置条件:工作区干净、已 npm login,建议在 main / dev 分支上操作。
# 预演(强烈建议先跑一遍):跑完整门禁、构建、pack 冒烟,并执行 npm publish --ignore-scripts --dry-run,不真正发布
npm run release:dry
# 正式发布
npm run release # 补丁版本,如 2.0.1 → 2.0.2
npm run release -- minor # 次版本,2.0.1 → 2.1.0
npm run release -- major # 主版本
npm run release -- 3.0.0 # 指定版本号
# 预发布通道
npm run release -- prerelease --preid beta --tag next
# 启用 npm 双因素验证(2FA)
npm run release -- --otp 123456注意:
npm run release后面追加参数需要加--(例如npm run release -- minor),否则 npm 不会把参数透传给脚本。
发布成功后会得到一个版本提交(消息为裸版本号,如 2.0.2)和对应标签(v2.0.2)。当前仓库未配置 git remote;如需推送,配置 remote 后执行 git push --follow-tags,或发布时加 --push 自动推送。
完整选项见 node scripts/release.mjs --help。
