@trustbaseai/shop
v0.1.0
Published
TrustBase 最小商域模型:商品与多规格、本地购物车、订单状态机(与链上一致)、订单草稿,以及跨侧硬契约的下单意愿 canonical JSON + sha256 + tb-auth memo
Maintainers
Readme
@trustbaseai/shop
TrustBase 的最小商域模型:商品与多规格、本地购物车、订单状态机(与链上一致)、订单草稿,
以及下单意愿(canonical JSON + sha256 + tb-auth: memo)的同构实现。
- 纯逻辑:无 UI、无 DOM、无存储、无网络、不签名、不持私钥。
- 同构:浏览器与 Node 跑同一份代码;
src/零 Node 内建依赖(测试扫着)。 - 唯一运行时依赖:
@noble/hashes(算意愿的 sha256)。 - 状态:已实现并测试通过(116 个用例);未验证/未做的清单见 §7。
1. 为什么这个包存在(以及它不是什么)
买家不装节点、浏览器不持私钥 —— 所以 v3.9.4 起 MsgCreateOrder 的 signer 是卖家,
"这一单是买家要的"由 tx memo 承载:memo = "tb-auth:<sha256(意愿原文)>"。
意愿原文的序列化口径是跨侧契约:卖家后端算一遍、买家浏览器算一遍、争议时仲裁方再算一遍,
三次必须得到同一个 hash。任何一方口径不一致 = 静默失败(链上只校验 64 位小写 hex,
不会告诉你"你的原文排序错了")。
本包就是那条口径的 SDK 侧实现,出处是链侧真源:
| 契约 | 链侧出处 |
|---|---|
| 意愿序列化 + sha256 + memo | trustbase-seller-backend/core/order-intent.js |
| MsgCreateOrder signer = seller / buyer 非空且 ≠ seller | trustchain-relmerge/proto/trustchain/order/v1/tx.proto:40-63、x/order/keeper/msg_server.go:37- |
| 订单状态与迁移 | proto/trustchain/order/v1/params.proto:11-19 + x/order/keeper/msg_server.go(守卫行号见 src/order.ts 顶部表) |
| tct1 地址形态校验 | trustbase-seller-backend/core/address.js |
不是什么:不是链客户端(不发交易、不签名 —— 那是 @trustbase/chain-client 与商户守护进程),
不是 UI(见 @trustbaseai/shop-ui),不是索引器(查询面见 @trustbase/index-client)。
2. 下单意愿:硬契约
import { buildOrderIntentHash } from '@trustbaseai/shop';
const { intent, canonical, hash, memo } = buildOrderIntentHash(
[{ sku_id: 'sku-tea-250g', quantity: 2, price_per_unit: '19900000' }],
{ buyer, seller, order_ref: 'tb-mf8k2p-3b1x9q', timestamp_ms: 1758500000000, session_ref: 'sess-abc' }
);
// canonical: 键递归升序的紧凑 JSON(被哈希的字符串,留证用)
// hash: 64 位小写 hex
// memo: "tb-auth:<hash>" ← 直接塞进 tx memo口径要点(与链侧逐条对齐):
| # | 规则 |
|---|---|
| 1 | 对象键递归升序、数组保序(items 顺序即业务顺序 → 顺序影响 hash) |
| 2 | quantity / price_per_unit 归一成字符串(2 与 "2" 同 hash) |
| 3 | 顶层固定 8 字段:kind, v, buyer, seller, order_ref, timestamp_ms, session_ref, items |
| 4 | 缺字段抛错,不补空串/0(否则两批不同订单会撞同一个 hash) |
| 5 | session_ref 是唯一例外:入参缺省 → ""(链侧 c.session_ref ?? "",有固定向量锁着) |
| 6 | memo 只接受 64 位小写 hex;非 tb-auth: 前缀的 memo 链上不校验(isValidAuthMemo 返回 true) |
比链侧更严的三处(只影响"更早报错",不影响合法输入的 hash):严格形状(多/少键即拒)、
金额必须是规范十进制整数字符串(小数直接拒)、buyer === seller 直接拒。
证据:与链侧同 hash(固定向量)
test-vectors/order-intent.golden.json 由 链侧真实现跑出来,不是手写的:
node tools/gen-intent-vectors.cjs ../trustbase-seller-backend # 或传绝对路径test/order-intent.test.ts 对每条向量断言 intent / canonical / hash / memo 逐字节相同,
另含两条链侧自算的交叉验证:normalization_cross_check(number 与 string 同 hash)、
key_order_cross_check(键插入顺序打乱后 hash 不变)。
该脚本不在
npm test里:链侧仓库不在本仓库,CI 跑不了不是 bug (与@trustbase/protocol的tools/check-proto-citations.cjs同一取舍)。 链侧口径变了就重跑一次 —— diff 出现在 review 里才是纪律。
3. 订单状态机(与链上一致,不是我们拍的)
PENDING ──confirm_payment──► PAID ──ship──► SHIPPED ──confirm──► CONFIRMED
│ │ │
└──cancel──► CANCELLED └────dispute────► DISPUTED ◄──dispute──┘| SDK 状态 | 链上枚举 | 值 |
|---|---|---|
| PENDING | ORDER_STATUS_PENDING | 0 |
| PAID | ORDER_STATUS_PAID | 7 |
| SHIPPED | ORDER_STATUS_SHIPPED | 1 |
| CONFIRMED | ORDER_STATUS_CONFIRMED | 2 |
| DISPUTED | ORDER_STATUS_DISPUTED | 3 |
| CANCELLED | ORDER_STATUS_CANCELLED | 5 |
链上还有 RESOLVED(4) / REFUNDED(6) 与 SettleOrder(CONFIRMED 后过争议窗口结算,不改状态)——
属于争议/结算工作包,本包不做(写在这里是为了让"为什么没有"有据可查)。
canTransition / assertTransition / canApplyAction / applyAction:前两个是状态对状态,
后两个是"动作 + 前置状态"(界面据此决定按钮可点)。
4. 商品与多规格
父商品 + N 个规格(SKU);链上仍是单 SKU 粒度(MsgCreateOrder.sku_id 是单个字符串),
所以"一个父商品 → N 条 MsgCreateOrder"这条路是通的。
- 上限是可配置参数:
ShopLimits.maxSkusPerGoods(默认20)、maxLinesPerOrder(默认20)。 边界语义:等于上限通过、超 1 即拒(limit_exceeded)。 - 金额一律 microTCT 十进制整数字符串(不是 TCT、不是浮点);
formatTctAmount负责显示(永远带单位)。
const goods = buildGoods({ goods_id, seller, title, skus: [{ sku_id, title, price_per_unit: '19900000', stock: 3, spec: { 规格: '250g' } }] });
defaultSku(goods); // 有货且最便宜
priceRange(goods); // { min, max }5. 购物车(本地)
纯函数、不可变、不碰任何存储 API(存哪儿是宿主的决定):
let cart = emptyCart();
cart = addToCart(cart, { goods, sku_id, quantity: 2 }); // 同 sku 合并;超库存抛 out_of_stock
cart = setLineQuantity(cart, sku_id, 0); // 0 = 删行
cartTotalMicroTct(cart); // 合计(整数串)
serializeCart(cart); / parseCart(raw) // 落盘/读盘(坏数据 → ok:false,不白屏)
cartToOrderLines(cart); // → 意愿 items(结算桥)6. 公开 API
| 模块 | 出口 |
|---|---|
| 意愿 | buildOrderIntentHash buildOrderIntent hashOrderIntent canonicalize(内部) buildAuthMemo isValidAuthMemo assertOrderIntent normalizeTimestampMs validateBuyerAddress + 常量 INTENT_KIND INTENT_SCHEMA AUTH_MEMO_PREFIX |
| 订单 | ORDER_STATUS ORDER_ACTIONS CHAIN_ORDER_STATUS ALLOWED_TRANSITIONS canTransition assertTransition canApplyAction applyAction isTerminalStatus buildOrderDraft orderDraftIntentItems orderDraftIntentContext createOrderRef |
| 商品 | buildGoods validateGoods resolveShopLimits skuById requireSku defaultSku priceRange totalStock specAxisNames skuLabel GOODS_STATUS + 上限常量 |
| 购物车 | emptyCart EMPTY_CART addToCart setLineQuantity removeFromCart clearCart cartLineCount cartTotalQuantity lineAmount cartTotalMicroTct cartToOrderLines serializeCart parseCart |
| 金额 | normalizeDecimalInteger normalizeCount isDecimalIntegerString addAmounts multiplyAmount formatTctAmount parseTctAmount MICRO_TCT_PER_TCT |
| 地址/错误 | isTctAddress TCT_ADDRESS_RE shortenTctAddress ShopError ShopErrorCode Validation<T> valid invalid |
错误分两类(与 @trustbase/protocol 同一套纪律):外部数据不合规 → validateXxx 返回 Validation<T>(不抛);
契约被破坏 → 抛 ShopError(code 机器可读,测试按 code 断言)。
7. 未验证 / 未做(诚实清单)
- 未验证:本包的意愿实现与链侧真机联调尚未做过(只有固定向量 + 代码口径对齐)。 等 WP0 升级窗口落地后,用真实卖家后端跑一单,比对两边 hash —— 那是最终验收。
- 未验证:真实链上多规格方案未定(链是单 SKU 粒度,本包只是"父商品 + N 规格"的模型)。
- 未做:
RESOLVED/REFUNDED状态与SettleOrder(结算)、退款式; - 未做:优惠券、多仓、库存并发预占、订单超时自动取消(链上有
auto_confirm_timeout,SDK 侧未接); - 未做:收货信息的加密与
shipping_address_hash—— 由商户守护进程做(浏览器不碰加密契约); - 未做:
shop与@trustbase/index-client的对接(索引器返回的CatalogItem→Goods的映射,属 WP5)。
8. 开发
npm run build # CJS → dist/ ;ESM → dist/esm/(浏览器可直接 import)
npm test # jest(116 用例)
npm run typecheck # tsc --noEmitsrc/ 的相对导入写 ESM 形态(带 .js 后缀)——这样 dist/esm/ 不需要打包器就能被浏览器加载;
jest 侧用 moduleNameMapper 摘掉后缀(见 jest.config.js)。
9. 约束(会拒绝的写法)
src/里出现node:/process./Buffer/__dirname→ 同构测试失败(构建配置也把types设为空,编译期就报);- 签名、私钥、助记词相关标识符 → 同构测试失败(本包不持私钥);
- 改意愿口径(排序、字段、归一)必须同时改链侧并 re-bump
INTENT_SCHEMA+ 重跑向量脚本。
