Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

DeepBook 开发权威指南

本目录是正式书稿与配套代码的工作区。

  • SUMMARY.md:全书目录。
  • OFFICIAL_DOCS_BASELINE.md:对齐 Sui 官方 DeepBookV3、Margin、Predict 文档的事实基线。
  • LEARNING_PATH.md:从易到难的六阶阅读路线。
  • SOURCE_DEFINITION_ATLAS.md:关键结构体、方法和事件的内嵌定义地图。
  • MOVE_READING_GUIDE.md:高级 Move + DeepBook 的阅读方法。
  • STYLE_GUIDE.md:章节写作规范。
  • chXX/README.md:每章导读、源码地图、小节目录和章节检查清单。
  • chXX/NN-title.md:每个编号小节的独立正文。
  • chXX/code/:每章配套代码。

本书以 Sui 官方文档作为产品和集成事实基线,以 MystenLabs/deepbookv3 的 GitHub 源码作为源码讲解基础,并把 MystenLabs/deepbook-sandbox 作为本地全栈实验环境,围绕 DeepBookV3、DeepBook Margin、DeepBook Predict、SDK 集成、Indexer、Sandbox 和生产应用开发展开。

写作目标不是复述文档,也不是堆源码链接,而是做成一本高级 Move 案例书 + DeepBook 专题书:先建立产品直觉和官方边界,再进入源码状态机,最后落到 SDK、PTB、Indexer、Server、风控和生产交付。

官方文档基线:本书如何对齐 Sui 文档

本书不是官方文档的改写版,也不是源码注释合集。官方文档给出产品定位、当前网络状态、公开对象和集成入口;本书在这个基线上继续做三件事:解释源码为什么这样设计,训练 Move 协议阅读能力,并把交易、SDK、Indexer、风控和部署串成可落地的应用路径。

本页按 2026-05-06 访问到的 Sui 官方文档整理,后续写作和事实核对都应回到这些页面确认。

DeepBookV3 的官方基线

官方定位:DeepBookV3 是构建在 Sui 上的去中心化中央限价订单簿。它利用 Sui 的并行执行和低交易费用,把高性能、低延迟的交易能力放到链上。官方还明确说明,DeepBookV3 本身不是面向终端用户的交易界面,而是为 DEX、钱包和其他应用提供内置交易能力。

本书采用的写法:

  • 先用订单簿产品语言解释 bid、ask、limit、market、maker、taker。
  • 再进入 Pool -> Book -> State -> Vault -> BalanceManager 的源码路径。
  • SDK 和 Indexer 不写成附录,而写成应用开发主线:SDK 构造交易,Indexer/Server 支撑行情、历史和风控。
  • DEEP tokenomics、staking、governance、fee discount 和 maker rebate 只在能落到源码或交易流程时展开。

官方入口:

DeepBook Margin 的官方基线

官方定位:DeepBook Margin 扩展 DeepBookV3 的交易能力,让用户通过借入资金建立杠杆仓位。官方文档的中心不是“多一个下单接口”,而是风险管理:抵押、借款、利息、oracle、风险率、清算和 TPSL 都是产品的一部分。

本书采用的写法:

  • 先解释用户路径:存抵押、借 base 或 quote、通过 DeepBook Pool 交易、还款或被清算。
  • 再解释对象关系:MarginPool 提供借贷流动性,MarginManager 包装 BalanceManagerMarginRegistry 管理池、版本、风险参数和启用状态。
  • 利息和清算按“可被用户观察的风险”来写,而不是只讲公式。
  • 每个 Margin 入口都必须回答:这一步增加风险还是降低风险,是否需要 oracle,是否允许 reduce-only,失败时归因到哪一层。

官方入口:

DeepBook Predict 的官方基线

官方定位:DeepBook Predict 是 Sui 上基于到期日的预测市场协议。它支持 binary positions、vertical ranges、oracle-based pricing、PredictManager 共享账户、Vault liquidity 和 PLP LP shares。

官方文档同时强调当前状态:Predict 是 Testnet integration surface,智能合约在 Mainnet 部署前可能变化。应用不应把旧 package id、旧对象布局或实验脚本当作稳定生产接口。

本书采用的写法:

  • Predict 章节必须持续标注网络和版本边界。
  • 产品模型按 oracle、expiry、strike、range、vault、PLP、settlement 展开,不套用 Spot 订单簿模型。
  • 应用集成采用三层数据模型:public Predict server 用于页面渲染和历史数据;checkpoint/event streaming 用于低延迟 oracle 状态;关键钱包流程前后再做直接 onchain object reads。
  • RangeKeyMarketKeyOracleSVIPredictManagerVault 是本章的核心定义,不应被 UI 字符串替代。

官方入口:

本书的出版级改写原则

官方文档适合快速集成和查对象。本书的章节必须在官方文档之上增加以下层次:

  1. 读者问题:这一节解决的是交易、风控、数据、SDK 还是 Move 安全问题。
  2. 官方定位:用官方文档确认能力边界、网络状态和公开集成入口。
  3. 源码定义:把关键 structpublic fun、事件或 handler 放进正文,而不是只给链接。
  4. 状态路径:说明对象如何被借用,余额如何移动,事件如何产生,失败如何 abort。
  5. 工程落地:把结论落实到 SDK、PTB、dry run、Indexer、UI、监控或部署。
  6. 风险边界:明确哪些能力是 Mainnet,哪些是 Testnet,哪些来自本书锁定源码快照,哪些仍需读官方最新文档确认。

如果一个小节只是在复述官方文档,它还不是书稿;如果一个小节只是在贴源码,它也还不是书稿。合格的小节应该让读者读完后,既知道官方能力边界,也能独立解释源码和实现一个可靠应用。

先读:从易到难学习路线

这本书的阅读顺序不按“源码文件复杂度”排列,而按学习者的能力爬坡排列。正确的节奏是:先知道 DeepBook 能做什么,再用 SDK 和最小交易跑通,再回头读 Move 源码,最后进入 Margin、Predict、Indexer 和生产系统。

六阶路线

阶段目标章节读完后应该能做什么
第一阶:建立直觉不急着读源码,先理解 CLOB、Sui Move 基础和 DeepBook 的产品边界ch00、ch01、ch02能解释 DeepBook 是什么,能查询池子对象,能读懂 Move 基本结构
第二阶:先用起来先通过 SDK、PTB 和最小应用跑通交易体验ch12、ch06能初始化 SDK,查询池子,创建 BalanceManager,构造限价单、市价单和撤单
第三阶:回到 Spot 核心源码在已经会用的基础上,精读现货核心ch03、ch04、ch05能把下单、撮合、结算、费用、治理和余额路径串起来
第四阶:高级组合和数据学 hot potato、闪电贷、事件索引和 Server 查询ch07、ch13能解释组合交易的资源闭环,能设计订单、成交、闪电贷和 Margin 事件索引
第五阶:扩展产品学 Margin 和 Predict 的协议结构与应用边界ch08、ch09、ch10、ch11能构建杠杆交易、风险面板、清算机器人或预测市场原型
第六阶:产品化交付把前面的能力收束成真实产品ch14、ch15能设计交易终端、做市机器人、数据服务,并完成测试、安全和部署清单

为什么先 SDK 后源码

直接从 pool.movebook.movestate.move 开始读,很容易把 DeepBook 读成一堆函数。更好的顺序是先完成一个最小体验:

  1. 查一个池子。
  2. 查订单簿。
  3. 创建或加载 BalanceManager
  4. 构造一笔 dry run 的限价单。
  5. 看到错误,再回到源码查原因。

这样读源码时,函数不再是孤立 API,而是你已经遇到过的交易步骤。TradeProofClocktype argumentsCoin<T>Balance<T>、abort code 和事件都会变成具体问题。

每章的四段式

后续写作要避免“机械源码解读”。每章应按四段推进:

  1. 场景问题:读者为什么需要这一章,例如“为什么下单前钱包有钱仍然失败”。
  2. 最小实验:先给一个可运行、可观察的动作,例如 query、dry run、PTB 或事件查询。
  3. 源码拆解:实验跑通后再进入 Move 源码,解释对象、资源、权限和事件。
  4. 工程升级:把源码结论落到 SDK、前端、Indexer、机器人、风控或生产部署。

如果一节只有源码结构,没有场景和实验,它就会像文档;如果一节只有代码调用,没有源码和失败边界,它就不够高级。

难度标尺

本书后续会用五个难度层级控制节奏:

  • L1 使用者:知道功能是什么,能查询和观察。
  • L2 集成者:能用 SDK/PTB 发起交易,并处理 dry run。
  • L3 源码读者:能沿 Move 调用链解释状态变化。
  • L4 协议构建者:能设计组合交易、权限边界和失败路径。
  • L5 生产负责人:能处理索引、监控、测试、安全和部署。

章节不是越靠后越好。高级读者也应该从 L1、L2 快速扫过,因为真实工程失败经常不是算法看不懂,而是对象、网络、权限、索引和用户体验没有接好。

关键定义地图:结构体、方法和事件怎么读

本页解决一个核心问题:读者不应该只看到 GitHub 链接。真正有效的源码讲解,必须把关键 struct、方法签名、字段含义、状态变化和常见误读直接放进书里。

本页代码摘录自 MystenLabs/deepbookv3@663edbf9c30d6c93100e6cd66936e1487a5dc9e0。链接只用于复核版本,正文解释以书内定义卡片为主。

本书后续所有源码讲解都按“定义卡片”写:

  1. 关键定义:摘出最小必要的 structpublic fun 或 handler。
  2. 字段解释:说明每个字段在协议状态中代表什么。
  3. 调用入口:列出最重要的方法,不堆完整 API。
  4. 状态变化:说明对象、资金、订单或事件如何变化。
  5. 常见误读:指出读者最容易混淆的边界。

01 Spot 入口:Pool 和 PoolInner

public struct Pool<phantom BaseAsset, phantom QuoteAsset> has key {
    id: UID,
    inner: Versioned,
}

public struct PoolInner<phantom BaseAsset, phantom QuoteAsset> has store {
    allowed_versions: VecSet<u64>,
    pool_id: ID,
    book: Book,
    state: State,
    vault: Vault<BaseAsset, QuoteAsset>,
    deep_price: DeepPrice,
    registered_pool: bool,
}

Pool 是外部应用传入 PTB 的共享对象。它很薄,只有对象身份和 Versioned 包装。真正业务状态在 PoolInnerbook 管订单簿,state 管账户、费用、历史和治理,vault 管 base/quote/DEEP 资产,deep_price 服务 DEEP 费用换算,allowed_versions 决定当前包版本是否还能操作这个池。

常见误读是把 Pool 当成“订单簿对象”。更准确的说法是:Pool 是一个交易入口对象,订单簿只是它内部的一部分。

02 Spot 写交易入口

public fun place_limit_order<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    balance_manager: &mut BalanceManager,
    trade_proof: &TradeProof,
    client_order_id: u64,
    order_type: u8,
    self_matching_option: u8,
    price: u64,
    quantity: u64,
    is_bid: bool,
    pay_with_deep: bool,
    expire_timestamp: u64,
    clock: &Clock,
    ctx: &TxContext,
): OrderInfo

这个签名已经解释了大半个交易模型:

  • self: &mut Pool:会改变池内订单簿、账户状态或 vault。
  • balance_manager: &mut BalanceManager:资金不直接从钱包扣,而从交易账户结算。
  • trade_proof: &TradeProof:证明当前调用者能代表这个 manager 交易。
  • pricequantityis_bid:订单意图。
  • order_typeself_matching_optionexpire_timestamp:执行策略和失败边界。
  • clock:订单过期、epoch、事件时间都依赖它。
  • 返回 OrderInfo:本次交易生命周期结果,而不是长期挂单对象。

读任何 Spot SDK 封装时,都应该反向映射到这个签名,检查对象、类型参数和整数单位是否完整。

03 账户抽象:BalanceManager、Cap、TradeProof

public struct BalanceManager has key, store {
    id: UID,
    owner: address,
    balances: Bag,
    allow_listed: VecSet<ID>,
}

public struct TradeCap has key, store {
    id: UID,
    balance_manager_id: ID,
}

public struct DepositCap has key, store {
    id: UID,
    balance_manager_id: ID,
}

public struct WithdrawCap has key, store {
    id: UID,
    balance_manager_id: ID,
}

public struct TradeProof has drop {
    balance_manager_id: ID,
    trader: address,
}

BalanceManager 是 DeepBook 的交易账户,不是钱包。balances 保存内部资产余额,owner 能管理权限和资金,allow_listed 保存被授权 cap 的 ID。TradeCapDepositCapWithdrawCap 把交易、存入、取出权限拆开。TradeProof 是交易内临时权限证明,只有 drop,不会长期保存。

关键方法:

public fun new(ctx: &mut TxContext): BalanceManager
public fun deposit<T>(balance_manager: &mut BalanceManager, coin: Coin<T>, ctx: &mut TxContext)
public fun withdraw<T>(balance_manager: &mut BalanceManager, withdraw_amount: u64, ctx: &mut TxContext): Coin<T>
public fun generate_proof_as_owner(balance_manager: &BalanceManager, ctx: &TxContext): TradeProof
public fun generate_proof_as_trader(balance_manager: &BalanceManager, trade_cap: &TradeCap, ctx: &TxContext): TradeProof

常见误读是“钱包里有 coin 就能下单”。DeepBook 非 swap 下单路径通常先要求资产进入 BalanceManager,然后交易通过 TradeProof 结算。

04 撮合核心:Order、OrderInfo、Fill

public struct Order has drop, store {
    balance_manager_id: ID,
    order_id: u128,
    client_order_id: u64,
    quantity: u64,
    filled_quantity: u64,
    fee_is_deep: bool,
    order_deep_price: OrderDeepPrice,
    epoch: u64,
    status: u8,
    expire_timestamp: u64,
}

public struct OrderInfo has copy, drop, store {
    pool_id: ID,
    order_id: u128,
    balance_manager_id: ID,
    client_order_id: u64,
    trader: address,
    order_type: u8,
    self_matching_option: u8,
    price: u64,
    is_bid: bool,
    original_quantity: u64,
    executed_quantity: u64,
    cumulative_quote_quantity: u64,
    fills: vector<Fill>,
    paid_fees: u64,
    maker_fees: u64,
    status: u8,
    market_order: bool,
    order_inserted: bool,
    timestamp: u64,
}

public struct Fill has copy, drop, store {
    maker_order_id: u128,
    maker_client_order_id: u64,
    execution_price: u64,
    balance_manager_id: ID,
    expired: bool,
    completed: bool,
    base_quantity: u64,
    quote_quantity: u64,
    taker_is_bid: bool,
    taker_fee: u64,
    maker_fee: u64,
}

三个对象的职责不同:

  • Order:会留在订单簿中的 maker 状态。
  • OrderInfo:本次 taker 或新订单的执行结果,函数返回它,事件也从它产生。
  • Fill:一次 taker 与 maker 的成交切片。

不要把 OrderInfo 当成订单簿里的长期订单。完全吃单的市价单可能只有 OrderInfoFill,不会变成 Order

05 资产保管和闪电贷:Vault、FlashLoan

public struct Vault<phantom BaseAsset, phantom QuoteAsset> has store {
    base_balance: Balance<BaseAsset>,
    quote_balance: Balance<QuoteAsset>,
    deep_balance: Balance<DEEP>,
}

public struct FlashLoan {
    pool_id: ID,
    borrow_quantity: u64,
    type_name: TypeName,
}

Vault 是池内资产保管层,撮合模块不直接转 coin。FlashLoan 是 hot potato:借出时返回 Coin<T>FlashLoan,归还时必须把同一个 FlashLoan 消费掉。

关键入口:

public fun borrow_flashloan_base<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    base_amount: u64,
    ctx: &mut TxContext,
): (Coin<BaseAsset>, FlashLoan)

public fun return_flashloan_base<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    coin: Coin<BaseAsset>,
    flash_loan: FlashLoan,
)

归还时校验三件事:pool_id 匹配,资产 type_name 匹配,归还数量等于 borrow_quantity。这不是 Margin 借贷,不存在跨交易债务。

06 Margin 核心:MarginManager、MarginPool、MarginRegistry

public struct MarginManager<phantom BaseAsset, phantom QuoteAsset> has key {
    id: UID,
    owner: address,
    deepbook_pool: ID,
    margin_pool_id: Option<ID>,
    balance_manager: BalanceManager,
    deposit_cap: DepositCap,
    withdraw_cap: WithdrawCap,
    trade_cap: TradeCap,
    borrowed_base_shares: u64,
    borrowed_quote_shares: u64,
    take_profit_stop_loss: TakeProfitStopLoss,
    extra_fields: VecMap<String, u64>,
}

MarginManager 是用户在单个 DeepBook Pool 上的保证金账户。它内部持有一个 DeepBook BalanceManager 和三种 cap,因此 Margin 交易仍然复用 Spot 的账户模型,只是在外层增加借贷、oracle、风险率和清算约束。

public struct MarginPool<phantom Asset> has key, store {
    id: UID,
    vault: Balance<Asset>,
    state: State,
    config: ProtocolConfig,
    protocol_fees: ProtocolFees,
    positions: PositionManager,
    allowed_deepbook_pools: VecSet<ID>,
    rate_limiter: RateLimiter,
    extra_fields: VecMap<String, u64>,
}

MarginPool 是某个资产的借贷池。vault 保存供应资产,state 管 shares、利息和借贷比例,allowed_deepbook_pools 决定哪些 DeepBook 池允许用这个资产做 Margin 交易。

07 Predict 核心:Predict 和交易入口

public struct Predict has key {
    id: UID,
    vault: Vault,
    fee_reserve: FeeReserve,
    treasury_cap: TreasuryCap<PLP>,
    pricing_config: PricingConfig,
    risk_config: RiskConfig,
    treasury_config: TreasuryConfig,
    oracle_config: OracleConfig,
    withdrawal_limiter: RateLimiter,
    trading_paused: bool,
}

Predict 是预测市场的共享对象。它不是订单簿;它围绕 oracle、价格区间、vault 风险、LP 份额和费用储备组织状态。

核心入口:

public fun mint<Quote>(
    predict: &mut Predict,
    manager: &mut PredictManager,
    oracle: &OracleSVI,
    key: RangeKey,
    quantity: u64,
    clock: &Clock,
    ctx: &mut TxContext,
)

public fun redeem<Quote>(
    predict: &mut Predict,
    manager: &mut PredictManager,
    oracle: &OracleSVI,
    key: RangeKey,
    quantity: u64,
    clock: &Clock,
    ctx: &mut TxContext,
)

mint 买入一个区间头寸,redeem 赎回活跃或已结算头寸。这里的难点不是撮合,而是 oracle 是否结算、vault 是否能承担风险、费用是否进入 fee reserve、LP 份额如何变化。

08 Indexer handler:事件如何变成表

#![allow(unused)]
fn main() {
define_handler! {
    name: OrderFillHandler,
    processor_name: "order_fill",
    event_type: OrderFilled,
    db_model: OrderFill,
    table: order_fills,
    map_event: |event, meta| OrderFill {
        event_digest: meta.event_digest(),
        digest: meta.digest(),
        checkpoint: meta.checkpoint(),
        pool_id: event.pool_id.to_string(),
        maker_order_id: event.maker_order_id.to_string(),
        taker_order_id: event.taker_order_id.to_string(),
        price: event.price as i64,
        base_quantity: event.base_quantity as i64,
        quote_quantity: event.quote_quantity as i64,
        onchain_timestamp: event.timestamp as i64,
    }
}
}

Indexer 不是“再查一次链上对象”。它把 Move 事件转成可查询的读模型。event_digest 通常由交易 digest 和 event index 组成,用来保证一笔交易中多个事件也能唯一落库。应用页面中的成交历史、K 线和账户流水,都要从这些事件语义推导。

后续写作要求

读者进入任何源码章节,都应该先看到本章相关的定义卡片,再看到调用链和链接。链接用于复核,不应该承担解释职责。

阅读说明:把 DeepBook 当作高级 Move 案例

这本书不是 DeepBook API 清单,也不是 Move 语法手册。更合适的读法是:把 DeepBook 当作一个真实、复杂、资金敏感的 Move 协议案例,用它反过来学习 Sui Move 的对象模型、资源约束、权限设计、组合交易、事件索引和生产工程。

阅读顺序以 从易到难学习路线 为准。章节目录中的文件编号是工程组织编号,不代表必须按源码复杂度硬读。

本书的三条阅读线

第一条线是产品线。你要知道 DeepBookV3、Margin、Predict、SDK、Indexer、Server 分别解决什么问题,哪些能力适合普通交易应用,哪些能力适合做市、清算、套利、预测市场或数据系统。

第二条线是源码线。每个关键功能都要落到 Move 文件、函数入口、对象字段、事件和 abort code。读源码时不要一页页顺读,而是沿一次交易走:对象从哪里来,谁能 mutably borrow,哪些资源被消费,哪些余额被 split 或 join,失败时哪个 assert 触发。

第三条线是工程线。真正写应用时,你要构造 PTB、做 dry run、处理对象版本、格式化整数、索引事件、展示用户能理解的错误,并在生产环境里监控资金和数据延迟。这些经验会穿插在每章的“Move 高阶穿插点”和小节提示中。

如何使用“Move 高阶穿插点”

每章导读中的“Move 高阶穿插点”不是额外阅读材料,而是本章的主线问题。阅读前先看这些提示,带着问题进源码。读完后回到提示,检查自己是否能用 DeepBook 的真实函数和对象回答。

例如,读 BalanceManager 时不要只记住它“管理余额”。你要问:

  • owner、trader、TradeCapDepositCapWithdrawCap 分别保护什么边界?
  • Coin<T> 何时进入协议内部变成 Balance<T>
  • 哪些函数需要 TradeProof,为什么不能只看交易发送者地址?
  • 事件如何让 Indexer 还原余额流水?

这类问题比背 API 更接近高级 Move 开发。

小节中的提示框

后续小节会逐步加入三类提示框:

Move 技巧:解释资源、泛型、ability、对象借用、hot potato、动态字段、事件和 abort code 的实际用法。

源码旁白:告诉你这段源码应该按什么顺序读,哪些名字容易误解,哪些字段决定状态迁移。

工程提醒:把源码结论落到 SDK、PTB、Indexer、UI、风控或生产监控中。

如果某一节看起来像文档说明,就要继续补这三类提示,直到它能回答“我读完后能写出更可靠的 Move 或 DeepBook 应用吗”。

建议的学习方式

每章至少做三件事:先画对象图,再追一条函数调用链,最后写一个最小实验。对象图帮助你理解所有权和共享对象;调用链帮助你理解状态迁移;最小实验帮助你暴露参数、权限和余额错误。

对 DeepBook 这类协议来说,真正的理解不是知道函数名,而是能解释失败路径。一个高级 Move 开发者应该能看到 abort code、事件缺失、余额不足、资源未消费或对象版本不匹配时,迅速判断问题属于哪一层。

读者目标

如果你是 Move 学习者,这本书会把真实协议当作练习场。你会看到资源模型如何服务金融安全,而不是停留在玩具例子。

如果你是 DeepBook 爱好者,这本书会帮你从产品使用进入协议理解。你会知道订单簿、Margin、Predict、闪电贷、Indexer 和 SDK 是如何组合起来的。

如果你是应用开发者,这本书的目标是让你能独立构建一个可靠的 DeepBook 应用,并知道每个工程决策背后的源码依据。

ch00 DeepBook 是什么

本章目标

本章先建立官方口径和读者直觉。官方文档把 DeepBookV3 定位为 Sui 上的去中心化中央限价订单簿,而不是一个面向终端用户的交易前端;它提供的是可被 DEX、钱包、做市系统和其他应用调用的链上交易基础设施。本书从这个定位出发,再把 Margin、Predict、SDK、Indexer 和 Server 放进同一张工程地图。

读完本章后,读者应该能回答三个问题:DeepBook 的核心产品边界是什么;Spot、Margin、Predict 分别解决哪类问题;如果要开发应用,应该从 SDK、直接 PTB、Indexer、Server 还是源码精读开始。

官方文档基线

方向官方定位本书展开方式
DeepBookV3Sui 上的 CLOB,提供限价单、市价单、SDK、Indexer、DEEP tokenomics、staking 和治理能力;本身不是交易 UI。先讲订单簿和账户模型,再精读 Pool -> Book -> State -> Vault,最后落到 SDK 和数据系统。
DeepBook Margin在 DeepBookV3 上扩展杠杆交易,核心是借贷、抵押、利息、风险率和清算。先讲用户风险,再读 MarginPoolMarginManagerMarginRegistry、oracle 和 liquidation。
DeepBook Predict基于 expiry 的预测市场协议,支持 binary positions、vertical ranges、oracle pricing、Vault liquidity 和 PLP。当前官方描述为 Testnet integration surface。先标注网络和版本边界,再讲 oracle、range key、vault、LP 和 public server 数据模型。

出版旁白:官方文档回答“当前能怎么集成”,本书回答“为什么这样设计,以及如何用源码和工程实践把它做对”。后续每章都会同时给出官方边界、关键源码定义和应用落地判断。

本章学习阶梯

  • L1 先回答“DeepBook 到底是什么”,不用读源码。
  • L2 用功能地图区分 Spot、Margin、Predict、SDK、Indexer 和 Server。
  • L3 找到每个功能背后的源码入口,但暂时只建立边界。
  • L4 根据自己的目标选择后续阅读路线。

官方入口与源码地图

本章只做功能总览,不做完整源码精读。官方链接用于确认产品边界和当前集成入口;GitHub 链接指向本书当前锁定的 DeepBook 源码提交。

功能域官方文档 / GitHub 源码本章关注点
DeepBookV3 官方说明Sui Docs: DeepBookV3CLOB、SDK、Indexer、DEEP tokenomics、无终端 UI 的产品定位
Margin 官方说明Sui Docs: DeepBook Margin杠杆交易、风险管理、抵押、利息和清算
Predict 官方说明Sui Docs: DeepBook PredictTestnet integration surface、binary/range、oracle、public server
DeepBook 总说明README.mdCLOB 定位、并行执行、低费用、闪电贷、治理、BalanceManager、DEEP
V3 现货核心packages/deepbook/sources/pool.move下单、撤单、swap、staking、治理、闪电贷、查询入口
账户抽象packages/deepbook/sources/balance_manager.moveowner、trader、cap、余额托管和权限证明
资产保管和闪电贷packages/deepbook/sources/vault/vault.moveFlashLoan、borrow/return、结算、DEEP burn
Marginpackages/deepbook_margin/sourcesMarginManager、借贷池、oracle、风险率、TPSL
Predictpackages/predict/sources预测市场、区间头寸、LP vault、oracle、费用储备
Indexercrates/indexer链上事件落库、订单成交、Margin 事件、闪电贷事件
Servercrates/serverREST 查询、行情、订单簿、成交、Margin 和组合数据

小节目录

本章代码

  • code/s01-feature-map/:把 DeepBook 功能域、源码路径、后续章节映射整理成一份可维护清单。

Move 高阶穿插点

  • 把 DeepBook 当作大型 Move 案例阅读:先判断对象所有权,再判断函数是否迁移状态。
  • 看到产品能力时同步问一句:它最终依赖哪种 Move 资源约束,是 key 对象、泛型资产、cap,还是 hot potato。
  • 读功能地图时不要只背模块名,要把每个模块关联到一个失败场景,例如权限错误、版本错误、资产方向错误或资源未消费。

常见错误

  • 直接把官方文档当作本书正文。官方文档是事实基线,本书必须继续解释源码、状态路径、失败场景和应用实现。
  • 只把 DeepBook 理解成“swap 接口”。DeepBook 的底层是订单簿,swap 是构造市价吃单的一种应用入口。
  • deepbook_margin 的借贷能力误认为 V3 spot 的普通订单能力。Margin 是独立包,调用 DeepBook 池完成杠杆交易。
  • 认为源码里没有闪电贷。当前锁定源码中,pool.move 暴露 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quotevault.move 持有具体实现。
  • 把 Indexer 当作链上真实状态。Indexer 是查询层,最终事实仍然来自链上对象、交易和事件。
  • 在产品设计阶段忽略 BalanceManager。大多数非 swap 交互都围绕它组织资金和权限。

本章检查清单

  • 能用三句话解释 DeepBook 的产品定位。
  • 能指出官方文档中 V3、Margin、Predict 的能力边界。
  • 能列出 DeepBookV3 的现货核心能力。
  • 能说明 Margin 和 Predict 与 V3 spot 的关系。
  • 能指出闪电贷源码所在的两个关键文件。
  • 能判断一个应用应该直接读链、接 SDK、接 Indexer,还是自建 Server。

进阶练习

  1. 画一张 DeepBook 功能地图,把用户、做市商、协议、Indexer、Server、SDK 和前端放进去。
  2. 阅读 pool.move,只记录所有 public fun 的名字,并按“交易、治理、查询、管理”分类。
  3. 阅读 crates/indexer/src/handlers,找出哪些事件服务现货,哪些事件服务 Margin。

ch00-01 DeepBook 解决什么问题

返回本章

DeepBook 是 Sui 上的去中心化中央限价订单簿,也就是 CLOB。它要解决的问题不是“如何用一条固定曲线完成资产兑换”,而是“如何把买卖双方的限价意愿放到链上,以价格优先、时间优先的方式撮合成交”。

在 AMM 中,交易者面对的是资金池曲线。价格由储备量和公式决定,流动性提供者把资产放进池子,交易者用滑点和手续费换取即时成交。DeepBook 的核心不同:它保存买盘和卖盘,maker 可以挂出具体价格和数量,taker 可以吃掉现有挂单,订单簿状态、成交、取消、费用和结算都通过链上对象和事件表达。

DeepBook 对开发者的价值主要有四类。第一,它提供链上撮合基础设施,协议和钱包不用自己实现订单簿。第二,它提供可组合的交易入口,应用可以选择限价单、市价单、直接 swap、批量撤单、查询订单簿等不同能力。第三,它提供账户和资金抽象,BalanceManager 让一个账户在多个池之间复用余额、权限和交易身份。第四,它提供数据层,事件、Indexer 和 Server 可以支撑行情、K 线、成交、账户历史和风控面板。

本书把 DeepBook 当成一套开发平台,而不只是一个交易页面。对于现货交易,你要理解 PoolBookStateVaultBalanceManager。对于应用开发,你要会用 SDK 构造交易、用 dry run 做交易前检查、用 Indexer 查询事件和历史数据。对于高级产品,你还要区分 DeepBookV3 spot、DeepBook Margin 和 DeepBook Predict:它们在同一个仓库中,但并不是同一个链上状态机。

一句话概括:DeepBook 是 Sui 生态中的链上订单簿和交易基础设施;DeepBookV3 是现货核心,Margin 和 Predict 是围绕交易、借贷、风险和定价扩展出的上层产品。

读源码时的第一条边界

先从 README.mdpackages/deepbook/sources/pool.move 建立产品模型,再进入 book/state/vault/ 等内部模块。否则很容易在撮合细节中迷失,看不清某个函数属于用户交易、协议管理、费用治理还是查询辅助。

本节检查

  • 能解释 DeepBook 和 AMM 的差异。
  • 能说明 DeepBook 对钱包、交易聚合器、做市商和后端服务的价值。
  • 能区分“现货核心”和“上层产品”。

ch00-02 为什么是链上中央限价订单簿

返回本章

中央限价订单簿的优势是价格表达精确。用户可以说“我只愿意用某个价格买入或卖出”,而不是被动接受曲线上的当前价格。对专业交易者、做市商、聚合器和清算机器人来说,这种表达能力比简单兑换更重要。

传统 CLOB 通常依赖中心化撮合引擎。中心化系统吞吐高、延迟低,但用户必须信任运营方管理订单、资产、成交和历史记录。链上 CLOB 的目标是把订单状态、资金结算和关键事件放到公开可验证的环境中。这样做的代价是所有操作都要遵守链上执行模型,撮合逻辑必须足够明确,状态设计也必须适配并行执行和对象借用。

Sui 的对象模型是 DeepBook 能成立的重要前提。一个交易池可以是 shared object,池内部再拆出订单簿、账户状态、费用和 vault。用户资金并不是随意散落在多个函数里,而是通过 BalanceManagerVault 在确定路径中流转。读源码时要始终记住:DeepBook 的核心不是“一个大 map 保存所有订单”,而是一组围绕对象所有权、共享对象和事件输出设计的状态机。

链上 CLOB 还给组合性带来优势。协议可以把 DeepBook 的 swap 入口嵌进自己的 PTB,做市商可以批量下单和撤单,Margin 可以把借出的资产送到 DeepBook 池完成杠杆交易,Indexer 可以从事件恢复成交和账户历史。只要对象和能力被正确传入,同一套链上规则可以服务多个产品。

但是链上 CLOB 不适合所有场景。如果应用只需要极小额、极简单的兑换,AMM 可能更直接。如果应用要求毫秒级私有撮合,链上执行也不是天然答案。DeepBook 适合的是需要公开流动性、可组合交易路径、可验证成交和链上结算的场景。

设计取舍

DeepBook 的源码大量体现这种取舍:Pool 是公开入口,Book 管订单簿读写,State 维护账户、历史、治理和费用,Vault 管资产保管和结算。这个拆分让交易路径可审计,也让后续章节可以按职责逐层阅读。

本节检查

  • 能说清 CLOB 的“价格表达能力”是什么。
  • 能解释为什么链上 CLOB 需要特殊状态设计。
  • 能指出 Sui 对象模型和 DeepBook 设计之间的关系。

ch00-03 功能地图

返回本章

DeepBook 的功能可以按三层理解:交易核心、扩展产品、数据和开发工具。

交易核心是 DeepBookV3 spot。它包含池创建、限价单、市价单、直接 swap、订单修改、订单取消、批量撤单、settled amount 提取、账户订单查询、level2 order book 查询、费用、staking、治理、rebate、referral、闪电贷和管理操作。核心源码集中在 packages/deepbook/sources

扩展产品主要是 DeepBook Margin 和 DeepBook Predict。Margin 引入 MarginManager、借贷池、抵押品、oracle、风险率、清算和 TPSL 条件订单。它不是现货池里的一个参数,而是独立 Move 包,通过 DeepBook 池完成杠杆交易。Predict 则是预测市场方向,包含区间头寸、LP vault、oracle 配置、风险配置、费用储备和结算逻辑。

数据和开发工具包括 TypeScript SDK、脚本、Indexer 和 Server。SDK 负责把复杂对象、类型参数和 PTB 组织成可调用接口;Indexer 负责把链上事件处理成数据库记录;Server 负责在数据库和 RPC 之上提供行情、订单簿、成交、资产、费用、Margin 状态、组合数据等查询接口。

可以把功能地图压缩成下面这张表:

层级典型功能主要源码
Spot 交易限价单、市价单、swap、撤单、查询、闪电贷pool.move
账户和资金余额、owner、trader、cap、proofbalance_manager.move
撮合和订单bid、ask、order id、fill、order infobook
费用和治理DEEP fee、staking、proposal、vote、rebatestate
Margin借贷、抵押、风险、清算、TPSLdeepbook_margin
Predict预测头寸、LP、oracle、settlementpredict
数据服务事件处理、REST API、行情和历史数据crates/indexercrates/server

怎么使用这张地图

读源码时先问三件事:这个功能是否改变链上资产,是否依赖 BalanceManager,是否需要 Indexer 才能高效查询。如果改变链上资产,优先读 Move;如果组织交易,读 SDK 和脚本;如果服务页面和后台,读事件、schema、server route。

本节检查

  • 能把 DeepBook 功能分成交易核心、扩展产品、数据工具三层。
  • 能根据一个需求快速定位源码目录。
  • 能解释为什么 Indexer 不是交易核心的一部分。

ch00-04 现货交易能力

返回本章

DeepBookV3 spot 的公开入口集中在 pool.move。从应用开发角度看,现货能力可以分为下单、兑换、订单管理、结算、治理和查询。

下单能力包括 place_limit_orderplace_market_order。限价单表达指定价格和数量,可能成交、部分成交,也可能进入订单簿等待后续撮合。市价单在源码中被转换成极端价格的 IOC 订单,用已有流动性尽量成交,未成交部分取消。订单参数会涉及方向、价格、数量、self match 选项、order type、过期时间和是否用 DEEP 支付费用。

兑换能力包括 swap_exact_base_for_quoteswap_exact_quote_for_base 以及带 manager 的版本。直接 swap 对协议集成很重要,因为调用者可以用 Coin 输入和输出,不必自己先管理完整 BalanceManager 生命周期。源码内部仍会走撮合路径,只是包装了临时资金管理和输出检查。

订单管理包括 modify_ordercancel_ordercancel_orderscancel_live_ordercancel_live_orderscancel_all_orders。这些函数服务交易端、做市端和风控后台。做市商通常关心批量撤单和批量重挂,普通用户更关心取消某一笔订单,清算或风控逻辑则需要理解 open orders 对余额和风险的影响。

结算能力包括 withdraw_settled_amountswithdraw_settled_amounts_permissionless。撮合完成后,用户可提取已结算资产和应得金额。DeepBook 不应被理解成“成交后余额立刻出现在钱包里”的模型;很多路径会先进入内部状态,再通过结算函数进入 BalanceManager

查询能力包括报价输出估算、mid price、level2 order book、账户 open orders、单个订单、订单详情、locked balance、pool params 等。这些链上查询函数和 Server 的 REST 查询互补:链上查询适合交易前验证,Indexer/Server 适合页面展示和历史数据。

本节检查

  • 能列出现货交易的六类能力。
  • 能解释 swap 与订单簿撮合之间的关系。
  • 能说明为什么成交和结算要分开理解。

ch00-05 BalanceManager 账户模型

返回本章

BalanceManager 是 DeepBookV3 最重要的开发者概念之一。它不是一个普通钱包地址,也不是某个池子的临时余额表,而是一个共享对象,用来保存单个交易账户在 DeepBook 中使用的多资产余额和权限。

源码入口是 balance_manager.move。从结构上看,它有 owner、allow list、余额集合、cap 和 proof 相关逻辑。owner 可以存入、取出、交易、staking 和管理交易权限;trader 通常不能取款,但可以执行交易相关动作。这样的设计适合做市团队、托管策略、自动化机器人和多交易员系统。

为什么 DeepBook 不直接让每次交易都从钱包 Coin 中扣款?原因是订单簿交易有挂单、部分成交、撤单、settlement、费用和 rebate。一个挂单可能长期存在,资金需要被锁定或记账;一个 maker 可能在多个池中同时做市;一个后台系统可能需要让多个 bot 共用同一套资金。BalanceManager 把这些复杂性收进一个账户抽象里。

BalanceManager 还提供 cap 模型。TradeCapDepositCapWithdrawCap 可以把交易、存入、取出权限拆开。源码中很多 DeepBook 操作需要 TradeProof,它是“当前调用者有权代表这个 BalanceManager 交易”的证明。这样做比到处传 owner 地址更清晰,也更适合上层协议组合。

应用开发时,BalanceManager 是你必须先设计的对象。交易终端要决定用户是否自己创建 manager;做市系统要决定一个策略一个 manager,还是多个策略共享 manager;托管系统要决定是否给机器人发 trader 权限;Margin 则把自己的 MarginManager 包装在 BalanceManager 和 cap 之上。

读源码的重点

先读 newnew_with_custom_owner_and_capsmint_trade_capdepositwithdrawgenerate_proof_as_ownergenerate_proof_as_traderdeposit_with_proofwithdraw_with_proof。这些函数解释了账户、权限和资产流转的骨架。

本节检查

  • 能解释 BalanceManager 和钱包地址的区别。
  • 能说明 owner、trader、cap、proof 的关系。
  • 能判断一个应用是否需要为用户创建 BalanceManager

ch00-06 Pool、Book、State、Vault 的分工

返回本章

DeepBookV3 的现货池可以从四个角色理解:Pool 是入口,Book 是订单簿,State 是账户和参数状态,Vault 是资产保管和结算。

Pool 位于 pool.move。它对外暴露用户和协议会调用的函数:创建池、下单、swap、撤单、staking、投票、claim rebates、闪电贷、burn DEEP、查询等。开发者阅读交易路径时应该先从 Pool 找入口,再跟进内部模块。

Book 位于 packages/deepbook/sources/book。它负责订单簿本身,包括 bid、ask、order、fill、order info 和撮合队列。这里最接近传统交易所撮合引擎的核心概念:价格优先、数量扣减、剩余挂单、成交记录和取消路径。

State 位于 packages/deepbook/sources/state。它不是“全局状态垃圾桶”,而是账户、历史、治理、费用参数、成交量和 rebate 计算的归属地。staking、proposal、vote、fee tier、maker rebate 等功能都需要读这里。

Vault 位于 vault.move。它保存 base、quote 和 DEEP 相关余额,负责把撮合结果沉淀为资产转移。闪电贷也在这一层实现,因为借出和归还必须直接约束池子的资产保管状态。

这四个模块的边界让源码更容易读。一个下单交易大致是:Pool 接收参数和权限证明,Book 执行撮合,State 更新账户、费用、成交量和历史,Vault 处理资产流入流出,最后通过事件和返回值暴露结果。

本节检查

  • 能说出四个模块各自负责什么。
  • 能解释为什么 Pool 是阅读入口但不是全部逻辑。
  • 能按模块描述一次下单的大致路径。

ch00-07 DEEP、费用、staking 与治理

返回本章

DeepBookV3 不只是一个撮合引擎,它还围绕 DEEP token 建立费用、staking、maker rebate 和池级治理机制。对应用开发者来说,这些机制直接影响交易前余额检查、费用展示、做市收益、治理页面和后台索引。

费用层面,DeepBook 的交易会涉及 maker fee、taker fee、DEEP 支付和 rebate。仓库 README 明确强调 DEEP 在费用和治理中的作用。源码中,费用参数、stake required、rebate 和历史成交量主要分布在 statetrade_params.movehistory.movegovernance.move

Staking 的作用有两类。第一,用户在某个池中 stake DEEP 后,可能获得费用和 maker rebate 相关权益。第二,stake 是参与治理的基础。池级治理不是随意改任意参数,而是围绕 taker fee、maker fee 和 stake required 等交易参数进行。

Pool 中的公开函数包括 stakeunstakesubmit_proposalvoteclaim_rebates。这说明 DeepBook 的治理和收益领取不是一个外部后台动作,而是合约公开接口的一部分。Indexer 也需要记录这些事件,否则前端很难展示用户历史、当前投票、rebate 领取和参数变化。

开发时最容易漏掉的是费用货币和交易前检查。一个用户有 base 和 quote 并不代表能成功下单,因为订单可能还需要 DEEP 支付费用。另一个常见错误是把 fee tier 写死在前端。更稳妥的做法是从链上或 Server 查询池参数、用户 stake 状态和费用相关数据,在交易前用 dry run 验证最终路径。

本节检查

  • 能说明 DEEP 在交易费用中的作用。
  • 能解释 staking 与治理的关系。
  • 能列出 Pool 中治理相关的公开函数。

ch00-08 闪电贷在源码中的位置

返回本章

DeepBookV3 当前锁定源码中存在闪电贷能力。它不在 deepbook_margin 包里,而是在现货核心的 PoolVault 路径中。

公开入口位于 pool.moveborrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote。这四个函数按资产方向拆开:base 和 quote 都可以借出,也都必须按对应类型归还。

具体实现位于 vault.moveVault 中定义了 FlashLoan hot potato 结构和 FlashLoanBorrowed 事件。borrow 函数会检查借款数量大于 0、vault 中有足够余额,然后把资产拆出为 Coin,同时返回一个 FlashLoan 证明。return 函数会校验 pool id、资产 type name 和归还数量,最后销毁 FlashLoan

理解这个设计要抓住 hot potato 模式:调用者拿到借出的 Coin 后,也拿到一个必须被处理掉的 FlashLoan 对象。Sui Move 的资源语义会迫使交易在同一个 PTB 中完成借出和归还,否则无法合法结束。这就是链上闪电贷的关键约束:不是靠后台追债,而是靠类型和资源生命周期保证原子性。

闪电贷和 Margin 借贷不同。Margin 的借款会形成持续债务、风险率、利息和清算路径;DeepBookV3 spot 的闪电贷是同交易内借出和归还,不形成长期 loan shares。读源码时不要因为都出现 borrow、loan 这些词就混在一起。

应用场景包括原子套利、组合清算、跨协议再平衡和无需预先持仓的复杂 PTB。但生产使用前必须验证池子流动性、归还数量、费用外部性、交易失败时的用户提示和潜在 MEV 风险。

本节检查

  • 能指出闪电贷的公开入口和实现文件。
  • 能解释 FlashLoan hot potato 的意义。
  • 能区分现货闪电贷和 Margin 持续借贷。

ch00-09 DeepBook Margin 是什么

返回本章

DeepBook Margin 是围绕 DeepBook 现货池构建的杠杆交易和借贷系统。它不是把 Pool 改成“带杠杆模式”,而是单独放在 packages/deepbook_margin/sources 中。

官方文档给 Margin 的第一层定位是“用借入资金放大交易能力”,但它的真正主线是风险管理。一个 Margin 应用如果只展示杠杆倍数,而不展示风险率、利率、清算阈值和 oracle freshness,就没有把协议讲完整。

Margin 的核心对象是 MarginManager。源码中的 margin_manager.move 显示,它包装了一个 BalanceManager,并保存对应的 DepositCapWithdrawCapTradeCap、借款 shares、deepbook pool id 和 TPSL 状态。也就是说,Margin 不是绕过 DeepBook 账户模型,而是把它作为交易账户底座。

Margin 还依赖借贷池、oracle、风险参数和清算逻辑。margin_pool.move 管供应、借款和利息;oracle.move 处理价格;margin_registry.move 管注册和配置;pool_proxy.move 负责把 Margin 交易接入 DeepBook 池;tpsl.move 提供 take profit / stop loss 条件订单。

从用户体验看,Margin 提供抵押、借款、杠杆买入、杠杆卖出、还款、平仓、清算和条件订单。从开发者角度看,更重要的是每个动作都需要风险检查:价格是否新鲜、风险率是否超过阈值、借款资产是否匹配、DeepBook pool 是否允许 Margin 交易、还款数量是否足够、清算者是否能获得奖励。

本书后续会把 Margin 分成协议源码和应用开发两章。第 08 章读核心 Move 状态机,第 09 章把它落到 SDK、PTB、CLI、清算机器人和风控面板中。

官方入口:

本节检查

  • 能解释 MarginManagerBalanceManager 的关系。
  • 能列出 Margin 依赖的模块:借贷池、oracle、registry、pool proxy、TPSL。
  • 能区分 Margin 借贷和现货闪电贷。
  • 能用风险率、利息和清算解释为什么 Margin 不是普通 spot 下单。

ch00-10 DeepBook Predict 是什么

返回本章

DeepBook Predict 是预测市场方向的独立产品包,源码位于 packages/predict/sources。它和现货订单簿的交易形态不同,不是简单买卖 base/quote,而是围绕某个 oracle、到期时间和价格区间生成预测头寸。

官方文档当前把 Predict 描述为 Testnet integration surface,而不是 Mainnet 稳定产品面。书中所有对象 ID、package ID、public server、entry points 和网络状态都必须带这个前提。预测市场章节不能只讲“源码里有什么”,还要讲“哪些能力可以按官方 Testnet 集成目标使用,哪些需要等部署和文档更新确认”。

主入口是 predict.move。源码注释说明该模块协调 vault、oracle config、manager 和 pricing 层,负责公开交易和 LP flows。事件包括 PositionMintedPositionRedeemedSuppliedWithdrawn、配置更新和 oracle 边界更新等。

Predict 的核心模型可以拆成五部分。第一,PredictManager 表示用户的预测头寸管理入口。第二,Vault 保存 LP 资金、头寸资产和风险暴露。第三,OracleConfigoracle 相关模块提供价格、区间和结算依据。第四,PricingConfigRiskConfigTreasuryConfig 控制费用、风险和金库参数。第五,FeeReserve 处理 LP、协议和保险费用分配。

这类产品对数据层要求很高。前端不只要展示“买入/卖出”,还要展示到期时间、strike 区间、oracle 状态、费用、vault 可用资金、最大赔付、LP 份额和结算状态。应用开发时,需要同时理解链上对象和索引数据,否则用户很难判断头寸价值。

官方集成模型建议组合三类数据:public Predict server 用于页面和历史数据,checkpoint/event streaming 用于低延迟 oracle 更新,关键钱包流程前后再直接读取链上对象。这个三层模型会贯穿 ch10、ch11、ch12 和 ch13。

本书会把 Predict 放在较后章节。原因是它需要读者先掌握 Move 对象、DeepBook 现货基础、SDK 交易构造、Indexer 和风控展示。直接从 Predict 开始会缺少必要上下文。

官方入口:

本节检查

  • 能说明 Predict 和现货订单簿的产品差异。
  • 能列出 Predict 的 manager、vault、oracle、pricing、fee reserve 五个核心区域。
  • 能解释为什么 Predict 前端强依赖数据层。
  • 能说明为什么 Predict 章节必须标注 Testnet / Mainnet 边界。

ch00-11 SDK、Indexer 与 Server

返回本章

DeepBook 开发不是只写 Move。真正的应用通常需要三类链下组件:SDK、Indexer 和 Server。

SDK 负责构造交易。DeepBook 的交易经常需要 shared object、类型参数、BalanceManager、cap、clock、registry、pool id、coin、order 参数和 dry run。手写 PTB 很容易传错对象、类型或顺序。SDK 的价值是把这些细节包装成业务动作,让应用代码表达“下限价单”“swap exact quote for base”“创建 MarginManager”“借款并交易”,而不是在每个页面重复拼交易。

官方 DeepBookV3 SDK 文档以 @mysten/deepbook-v3 为主入口,强调 constants、coin/pool/manager key 配置和 DeepBookClient。本书会在此基础上补齐生产应用需要的部分:钱包签名、对象刷新、dry run、错误解析、网络隔离和服务端交易构造边界。

Indexer 负责处理历史事实。链上事件是 DeepBook 数据的关键来源,包括订单更新、成交、池创建、参数更新、stake、proposal、vote、rebate、闪电贷、Margin 借款、还款、清算、抵押、供应和 Predict 事件。源码中 crates/indexer/src/handlers 展示了大量事件处理器。没有 Indexer,前端仍能直接读链上对象,但历史成交、K 线、账户活动和筛选查询会很困难。

官方 Indexer 文档提供 public mainnet/testnet endpoint、资产最小单位转换、pool 信息、historical volume、OHLCV 等 API。书中不会把 public endpoint 当作无限可用的生产依赖;专业交易终端和机器人需要明确延迟、健康检查、自建服务和降级策略。

Server 负责把数据库和 RPC 组合成查询 API。crates/server/src/server.rs 中可以看到 pools、historical volume、ticker、trades、orders、assets、OHLCV、level2、fees、status、Margin events、portfolio 等 route。它服务的不是链上执行,而是产品体验和后台系统。

三者的关系可以这样记:SDK 写入交易,Indexer 读取事件,Server 提供查询。一个最小交易工具可能只需要 SDK 和 RPC;一个专业交易终端通常需要 SDK、Indexer 和 Server;一个风控系统还需要自定义数据库视图、告警和任务调度。

本节检查

  • 能解释 SDK、Indexer、Server 各自解决的问题。
  • 能判断一个页面数据应该来自链上对象、Indexer 表,还是 Server API。
  • 能说明为什么历史数据不能只靠实时 RPC 查询。
  • 能说明 public indexer 和自建 indexer 的取舍。

ch00-12 可以构建哪些应用

返回本章

理解 DeepBook 的最好方式之一,是从可以构建的应用倒推需要学习哪些模块。

最直接的是交易终端。它需要展示池列表、订单簿、成交、K 线、用户余额、open orders、历史订单、下单面板、撤单和订单状态。交易提交依赖 SDK 和 BalanceManager,行情展示依赖 Indexer 和 Server,交易前检查依赖链上查询和 dry run。

第二类是 swap 和聚合器。它不一定暴露完整订单簿,而是把 DeepBook 作为流动性来源之一。应用需要估算输出数量、检查 min out、构造 swap_exact_base_for_quoteswap_exact_quote_for_base,并处理 DEEP 输入和剩余 coin 输出。聚合器还要比较 AMM、CLOB 和其他路由。

第三类是做市和量化工具。它需要批量下单、批量撤单、实时订单簿、账户 open orders、成交事件、费用和 rebate 统计。做市系统通常还会管理多个 BalanceManager 或多个 trader 权限,并对异常成交、撤单失败和网络延迟做监控。

第四类是 Margin 应用。它要支持创建 MarginManager、存入抵押品、供应借贷池、借入资产、通过 DeepBook 池执行杠杆交易、还款、平仓、TPSL 和清算。风控 UI 必须显示风险率、债务、资产、oracle 时间戳和清算阈值。

第五类是数据服务。团队可以基于 Indexer 和 Server 构建行情 API、交易历史 API、研究面板、费用统计、rebate 报表、清算监控和组合资产视图。这类应用不一定提交交易,但对事件语义和表结构要求很高。

第六类是协议组合。其他 Move 包可以在 PTB 中调用 DeepBook swap 或闪电贷,完成套利、再平衡、清算、结构化产品或 treasury 管理。此时最重要的是对象传递、资产归还、失败回滚和交易原子性。

本节检查

  • 能列出六类 DeepBook 应用。
  • 能为每类应用选择需要学习的章节。
  • 能判断一个需求是交易构造问题、链上状态问题,还是数据索引问题。

ch00-13 本书阅读路线

返回本章

本书按“产品理解、Move 基础、现货核心、SDK 应用、Margin、Predict、Indexer、生产化”的路线组织。

如果你是第一次接触 DeepBook,建议顺序阅读。第 00 章和第 01 章建立产品、术语和环境。第 02 章补齐 Sui Move 开发基础。第 03 到第 06 章进入 DeepBookV3 spot 核心,重点读 PoolBookStateVaultBalanceManager、费用和治理。第 07 章把源码知识落到现货应用开发。

如果你的目标是 Margin,从第 00、01、02、03、05 章读到足够理解 BalanceManager 和现货池,再进入第 08、09 章。不要跳过现货基础,因为 Margin 的交易最终仍会通过 DeepBook Pool 和内部 BalanceManager 能力完成。

如果你的目标是 SDK 集成,先读第 00、01、03、05、07、10 章。你不必一开始精读所有 Move 内部细节,但必须知道交易需要哪些对象、费用和权限,以及 dry run 失败时应该回到哪里查源码。

如果你的目标是 Indexer 和数据服务,先读第 00、03、04、05、11、12 章。重点不是“每个函数怎么撮合”,而是事件何时发出、字段语义是什么、数据库表如何还原产品视图、Server API 如何组合链上和库内数据。

如果你的目标是 Predict,先读第 00、02、10、11、12 章,再进入第 13、14 章。Predict 有自己的 oracle、vault、pricing、risk 和 fee reserve,需要比现货交易更多的产品上下文。

最后,第 15 章把前面的内容收束为生产应用:错误处理、dry run、监控、索引延迟、资金安全、对象版本、权限管理、部署和测试。读完后,你应该能独立设计一个基于 DeepBook 的交易或数据应用,并知道每个模块的源码根据在哪里。

本节检查

  • 能按自己的目标选择阅读路线。
  • 能解释为什么 Margin 和 Predict 不适合脱离现货基础直接学习。
  • 能把“源码精读”和“应用开发”连接起来。

ch01 DeepBook 与链上订单簿入门

本章目标

第一章的任务不是把所有源码讲完,而是帮读者建立正确的入口。DeepBook 不是 AMM,也不是一个单独的前端产品;它是一组围绕链上订单簿、账户托管、事件索引和 SDK 集成组织起来的交易基础设施。

读完本章后,读者应该能用自己的话区分 CLOB 与 AMM,能在本地找到 DeepBookV3、Margin、Predict、Indexer 和 Server 的源码边界,能画出一次下单从钱包签名到 PoolBalanceManager、事件和 Indexer 的路径,并完成一次最小池子对象查询。

本章学习阶梯

  • L1 建立 CLOB、AMM、订单、池子和角色直觉。
  • L2 完成环境检查和第一个池子对象查询。
  • L3 把查询结果映射到 Sui object、type、shared owner 和网络配置。
  • L4 能用自己的话画出一次下单从钱包到事件的路径。

源码地图

本章只读取项目入口和产品边界,不进入撮合细节。源码地图的作用是建立导航能力:读者知道后续遇到订单、余额、事件、SDK 或 Server 时该回到哪个目录。

主题GitHub 源码阅读重点
仓库入口README.mdDeepBookV3 的 CLOB 定位、BalanceManager、Pool、DEEP 费用和治理
DeepBook Move 包packages/deepbook/README.md和仓库入口一致,是合约包级别说明
DeepBookV3 合约packages/deepbookSpot CLOB、BalanceManager、Pool、Book、State、Vault
DeepBook Marginpackages/deepbook_margin保证金和普通借贷,不等同于 DeepBookV3 闪电贷
DeepBook Predictpackages/predict预测市场相关包,版本和网络状态以后按源码核对
Indexercrates/indexer从链上事件落库
Servercrates/server对外查询服务
运维脚本scripts/transactions创建池子、升级、参数更新和做市脚本

小节目录

Move 高阶穿插点

  • 环境搭建不只是安装工具,还要建立“源码、链上对象、SDK、Indexer”四个视角之间的跳转习惯。
  • 查询池子对象时,优先记录对象 ID、type、version、owner 和 shared object 状态,这些字段决定后续 PTB 能否正确构造。
  • 遇到 DeepBook 术语时,立刻追问它是链上对象、事件字段、SDK 配置,还是纯前端展示概念。

常见错误

  • 把 DeepBookV3 当成 AMM,只查储备量,不查订单簿、订单和事件。
  • packages/deepbook_margin 的借贷逻辑误认为 DeepBookV3 spot 交易路径。
  • 在 mainnet 对象 ID 上使用 testnet RPC,导致对象不存在。
  • 构造交易前没有确认 BalanceManager 所属 owner、cap 和交易者关系。
  • 只看前端返回值,不订阅或索引 event::emit 产生的链上事件。

本章检查清单

  • 能指出 DeepBookV3 源码包路径。
  • 能解释 CLOB 和 AMM 的交易路径差异。
  • 能画出钱包到 PoolBalanceManager、事件、Indexer 的路径。
  • 能运行环境检查命令。
  • 能查询一个池子对象并确认网络一致。

进阶练习

  1. 用文字画出一次限价买单从钱包签名到 OrderPlaced 事件的路径。
  2. 阅读 packages/deepbook/sources/pool.move,列出 place_limit_order 的每个参数含义。
  3. 阅读 packages/deepbook/sources/balance_manager.move,解释为什么 owner 和 trader 的权限不同。

ch01-01 本书定位

返回本章

先看问题

这一节不急着进源码。先用“本书定位”回答一个更基础的问题:如果要构建真实交易应用,哪些概念必须先讲清楚,哪些细节可以等到后面再拆。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

本书面向已经会写基础程序、准备进入 Sui Move 和 DeepBook 应用开发的读者。你会同时接触五类工程任务:Move 合约阅读、TypeScript SDK 交易构造、后端服务、Indexer 数据表、前端交易面板。

本章先建立全局坐标。源码入口是 README.mdpackages/deepbook/README.md。这两个文件明确 DeepBookV3 是构建在 Sui 上的 CLOB,并说明 BalanceManagerPool 和 DEEP 费用的角色。

开发实践上,后续每章都会绑定 GitHub 源码,不把“交易所”“账户”“事件”停留在概念层。读者应把书稿仓库 deepbook-bootcamp 当作练习区,把 MystenLabs/deepbookv3 当作权威源码。

阅读补充

定位这本书时,先把“读源码”和“构造交易”放在同一条线上:概念章节负责建立对象边界,Move 章节负责读懂资源和能力,架构章节开始把 PoolBookStateVaultBalanceManager 串成交易路径。这样后续遇到 SDK、Indexer 或前端示例时,不会把链上对象和链下读模型混成一个账户系统。

落地判断

  • 每章练习都要能回到 DeepBookV3 源码路径,而不是只引用书稿里的摘要。
  • 遇到“账户”“池子”“订单”这类词,先判断它在 Move 对象、事件还是链下表里出现。
  • 记录本书练习区和权威源码区的路径,避免在 bootcamp 仓库里修改上游协议源码。

读完以后问自己

  • 这本书的权威协议源码入口是哪两个 README?
  • 为什么第一章不直接从撮合算法开始?
  • 遇到书稿与源码不一致时,应该以哪个目录为准?

ch01-02 DeepBook 是 CLOB,不是 AMM

返回本章

先看问题

先把“DeepBook 是 CLOB,不是 AMM”放到读者路径里看:你不是在背一个协议名,而是在建立一套判断 DeepBook 能做什么、不能做什么的坐标。读这一节时,重点看产品边界如何落到对象、交易和数据系统上。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

CLOB 的核心是挂单簿。买单和卖单按价格、时间或订单 ID 排序,taker 的新订单进入撮合路径,maker 的剩余订单可能留在簿上。AMM 的核心是资金池曲线,交易者和池子交换资产,价格由曲线和储备量推导。

DeepBookV3 的入口说明在 README.md 中直接称其为 decentralized central limit order book。合约入口位于 packages/deepbook/sources/pool.move,其中 place_limit_orderplace_market_order 都返回 OrderInfo,说明交易结果不是简单的 swap 数值,而是订单生命周期的一部分。

开发时不要用 AMM 的“输入 coin,输出 coin,路径结束”来理解所有 DeepBook 操作。DeepBookV3 支持直接 swap,但常规交易会经过 BalanceManagerTradeProofPoolBookStateVault,并生成订单和成交事件。

阅读补充

阅读 CLOB 路径时,从 pool.move 的下单入口开始,看它如何把订单参数转成 OrderInfo,再交给 book.move 与对手盘匹配。AMM 的核心状态通常是池内储备和曲线公式;DeepBook 的核心状态还包括价格档位、订单 ID、maker/taker 账户影响和事件流。

直接 swap 只是 DeepBook 暴露的一种使用方式,不代表底层退化成 AMM。开发者在前端展示成交结果时,仍要考虑订单簿深度、部分成交、剩余挂单和订单事件。

落地判断

  • 不要用 AMM 的 reserve ratio 推断 DeepBook 报价,必须读取订单簿或查询服务给出的深度。
  • 构造限价单时写清 is_bid、价格、数量、订单类型和 BalanceManager 授权。
  • 调试成交差异时同时看 OrderInfo、fill 事件和 Vault 净额结算。

读完以后问自己

  • CLOB 的价格来自哪里,AMM 的价格又来自哪里?
  • 一笔限价单为什么可能产生部分成交和剩余挂单?
  • DeepBook 的 swap 入口为什么仍需要理解订单簿?

ch01-03 CLOB、AMM、RFQ 和聚合器

返回本章

先看问题

这一节不急着进源码。先用“CLOB、AMM、RFQ 和聚合器”回答一个更基础的问题:如果要构建真实交易应用,哪些概念必须先讲清楚,哪些细节可以等到后面再拆。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

CLOB 路径:钱包签名 PTB,传入 Pool<BaseAsset, QuoteAsset>BalanceManagerTradeProof、价格、数量和方向,链上撮合后更新订单簿和账户状态,并发出事件。

AMM 路径:钱包签名交易,传入池子和输入资产,合约按曲线计算输出。交易主要关注滑点、储备量和手续费。

RFQ 路径:报价方离线或链下给出价格,交易者接受报价后链上结算。核心风险在报价有效期、签名校验和对手方流动性。

聚合器路径:聚合器把多个 CLOB、AMM 或 RFQ 拆成一笔或多笔路由。应用开发者仍要识别底层协议类型,因为错误处理、事件解析和资金结算完全不同。

DeepBookV3 的交易入口在 packages/deepbook/sources/pool.move;撮合核心在 packages/deepbook/sources/book/book.move;订单事件在 packages/deepbook/sources/book/order_info.movepackages/deepbook/sources/book/order.move

阅读补充

聚合器接入 DeepBook 时,通常不会只关心“能不能换币”,还要关心可成交深度、滑点保护和交易失败后的回退路径。CLOB 给出的报价依赖当前订单簿状态;RFQ 依赖做市商报价承诺;AMM 依赖曲线和储备,因此同一个前端路由需要把三类风险拆开处理。

源码阅读可以先看 order_query.move 和 server/indexer 的查询边界,再回到 pool.move 的交易入口。这样能分清“报价读模型”与“最终执行交易”分别来自哪里。

落地判断

  • 聚合器报价要带上时间、对象版本或 digest 上下文,避免展示过期深度。
  • 执行前仍要在交易参数里设置可接受价格或数量边界,不能信任链下报价恒定有效。
  • 把 DeepBook 事件纳入成交回放,方便解释路由结果和用户实际到账。

读完以后问自己

  • CLOB、AMM、RFQ 的报价可信来源分别是什么?
  • 聚合器调用 DeepBook 时,哪部分是链下查询,哪部分是链上执行?
  • 订单簿深度变化会怎样影响路由失败率?

ch01-04 产品边界:V3、Margin、Predict

返回本章

先看问题

先把“产品边界:V3、Margin、Predict”放到读者路径里看:你不是在背一个协议名,而是在建立一套判断 DeepBook 能做什么、不能做什么的坐标。读这一节时,重点看产品边界如何落到对象、交易和数据系统上。

本节坐标

本节先用官方文档定边界,再对照源码包。官方文档给出产品定位和当前集成状态,源码包解释这些定位如何落到对象和函数。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

DeepBookV3 是 spot CLOB。官方文档还强调它本身不是终端用户交易界面,而是供 DEX、钱包、做市系统和其他应用集成的链上交易基础设施。源码包在 packages/deepbook,核心对象包括 PoolBalanceManagerBookStateVault

DeepBook Margin 在 packages/deepbook_margin。官方定位是杠杆交易扩展,核心是借款、抵押、风险率、利息和清算。不要把 Margin 的普通借贷事件写成 DeepBookV3 闪电贷。DeepBookV3 闪电贷入口后续会在 pool.movevault/vault.move 中单独分析。

DeepBook Predict 在 packages/predict。官方当前把它标为 Testnet integration surface,因此本书描述 Predict 时必须持续标注网络状态、public server、package/object ID 和 Mainnet 前可能变化的边界。

阅读补充

DeepBookV3 的本体是 spot CLOB,deepbook_marginpredict 是相邻产品,不应在第一遍阅读时把状态机合并。尤其是 V3 的闪电贷 hot potato 与 Margin 的借贷仓位不是同一种债务模型,混用术语会直接导致交易路径和风险解释错误。

做源码地图时,先按包目录划边界,再看包之间是否通过对象、cap 或事件产生依赖。没有明确调用关系时,文档应写“相邻产品”或“后续章节入口”,不要推断为同一个协议状态。

落地判断

  • 涉及普通 spot 交易时优先引用 packages/deepbook,不要引用 Margin 仓位逻辑解释成交。
  • 提到闪电贷时明确是 vault::FlashLoan hot potato,还是 Margin 借贷。
  • Predict 相关内容先标为预测市场包入口,等后续章节按源码确认具体流程。

读完以后问自己

  • DeepBookV3、Margin、Predict 在本地分别对应哪个包?
  • V3 闪电贷和 Margin 借贷最容易混淆的地方是什么?
  • 如果事件表里同时有交易和借贷事件,应按什么字段或模块区分?

ch01-05 为什么 Sui 对象模型适合订单簿

返回本章

先看问题

这一节不急着进源码。先用“为什么 Sui 对象模型适合订单簿”回答一个更基础的问题:如果要构建真实交易应用,哪些概念必须先讲清楚,哪些细节可以等到后面再拆。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

订单簿交易的难点是高并发:不同交易对、不同账户、不同订单簿层级最好能并行执行。Sui 的对象模型把状态显式放到对象里,交易声明要读写哪些 owned object、shared object 和 immutable object,执行层可以据此判断并行边界。

DeepBookV3 把交易对封装成 Pool<BaseAsset, QuoteAsset>,定义在 packages/deepbook/sources/pool.movePoolhas key 的链上对象,内部持有 Versioned,真实字段在 PoolInnerbookstatevaultdeep_price 和版本集合。

实践上,应用构造交易前要先确定会触碰哪些对象:交易对池子、用户 BalanceManagerClock、可能的 coin 或 cap。对象选择错误会导致 dev inspect 或执行时 abort。

阅读补充

DeepBook 的关键拆分是:池子本身是共享对象,用户交易账户由 BalanceManager 表示,权限通过 cap/proof 传入。这样交易必须串行访问同一个池的撮合状态,但不同用户的余额授权和链下查询可以有更清晰的对象边界。

阅读对象模型时,重点看函数签名中的 &mut Pool&mut BalanceManager&Clock 和 capability 参数。签名往往比正文注释更直接地告诉你哪些状态会被写、哪些状态只读、哪些权限必须由调用者提供。

落地判断

  • 分析并发时先列出 mutable shared object,而不是只看函数名。
  • 把 UID/ID、owned/shared/capability 的区别写进交易构造说明。
  • 订单簿数据结构可以复杂,但入口对象借用关系必须先画清楚。

读完以后问自己

  • 为什么同一个池的下单需要访问 shared Pool
  • BalanceManager 的拆分给用户授权带来什么好处?
  • 函数签名里哪些参数最能体现并行边界?

ch01-06 核心用户角色

返回本章

先看问题

先把“核心用户角色”放到读者路径里看:你不是在背一个协议名,而是在建立一套判断 DeepBook 能做什么、不能做什么的坐标。读这一节时,重点看产品边界如何落到对象、交易和数据系统上。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

taker 是主动吃掉订单簿流动性的人。maker 是提供挂单流动性的人。LP 在 DeepBook 语境里通常指通过订单或策略提供流动性的一方,而不是 AMM LP token 的持有人。

borrower 和 liquidator 更多出现在 Margin 或闪电贷场景。DeepBookV3 spot 交易中,闪电贷是同一交易内借出和归还的 hot potato 模式;Margin 的借贷状态另属 packages/deepbook_margin

predict trader 则属于 packages/predict 的业务边界。不要把这些角色混到一个对象模型里;先看包目录,再看事件和状态结构。

阅读补充

角色不是前端标签,而是权限和对象关系。交易者可能是 BalanceManager owner,也可能是持有 TradeCap 的被授权方;管理员通过 admin cap 管理版本、池注册和参数;Indexer/server 只读取事件和对象,不应被当成链上权限来源。

做权限设计时,把“谁签名”“谁拥有 BalanceManager”“谁持有 cap”“谁承担链下展示”拆成四列。这样能提前发现代客交易、应用授权和只读查询服务之间的边界。

落地判断

  • 交易者身份以 BalanceManager owner/cap/proof 校验为准,不以前端登录态为准。
  • 管理员操作要单独标注 cap 来源和可修改的协议参数。
  • Indexer/server 只能解释历史和读模型,不能替代链上授权检查。

读完以后问自己

  • maker 和 taker 在链上一定是不同地址吗?
  • 应用拿到 TradeCap 后能绕过 BalanceManager 校验吗?
  • 管理员、应用、Indexer 三类角色的信任边界有什么不同?

ch01-07 全局心智模型

返回本章

先看问题

这一节不急着进源码。先用“全局心智模型”回答一个更基础的问题:如果要构建真实交易应用,哪些概念必须先讲清楚,哪些细节可以等到后面再拆。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

一次 DeepBookV3 spot 交易可以按下面路径理解:

wallet -> PTB -> Pool<Base, Quote>
              -> BalanceManager + TradeProof
              -> Book match/place
              -> State account/history/governance
              -> Vault settlement
              -> event::emit
              -> Indexer -> Server/API -> frontend

Poolpackages/deepbook/sources/pool.move,负责公开交易入口。BalanceManagerpackages/deepbook/sources/balance_manager.move,负责用户资金和交易证明。Bookpackages/deepbook/sources/book/book.move,负责订单簿读写和撮合。Vaultpackages/deepbook/sources/vault/vault.move,负责最终资产结算。

Indexer 不应该从前端猜订单状态,而应消费事件。订单相关事件定义在 book/order_info.movebook/order.move,余额事件定义在 balance_manager.move

阅读补充

全局路径可以简化为:钱包构造交易,传入 PoolBalanceManager 授权;Pool 调用 Book 完成撮合;State 更新账户、费用和历史;Vault 做净额资产结算;事件被 Indexer 消费成链下查询模型。每一步都对应不同源码目录,不要用一个“成交成功”概括。

阅读时建议画两条线:控制流从 pool.move 进入撮合和状态处理,资金流从 BalanceManagerVault 的差额结算展开。事件流则独立标注,因为它服务于查询和审计,不参与交易原子性本身。

落地判断

  • 画架构图时同时标注控制流、资金流和事件流。
  • 定位 bug 时先判断是链上执行失败、资金不足、事件索引延迟还是前端读模型错误。
  • 不要把 Indexer 数据当作交易成功的先决条件,交易成功由链上执行决定。

读完以后问自己

  • 一次下单的控制流和资金流分别经过哪些模块?
  • 为什么事件流需要单独画出来?
  • 前端看到订单状态延迟时,首先该怀疑链上还是 Indexer?

ch01-08 网络使用方式

返回本章

先看问题

先把“网络使用方式”放到读者路径里看:你不是在背一个协议名,而是在建立一套判断 DeepBook 能做什么、不能做什么的坐标。读这一节时,重点看产品边界如何落到对象、交易和数据系统上。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

本书默认用GitHub 源码解释机制,用 testnet 或 mainnet 查询真实对象,用 localnet 做可控实验。主网适合读对象和事件,不适合随意发交易。测试网适合发交易练习,但对象 ID 和包版本会变化。本地网适合 Move 包发布、单元测试和失败案例复现。

脚本配置中可见网络常量,例如 scripts/config/constants.ts 保存了 mainnet/testnet 的 admin cap、upgrade cap 等对象 ID。这些 ID 是脚本运维材料,不等同于所有池子清单。

开发示例中优先通过环境变量传入对象 ID,例如 POOL_IDRPC_URLNETWORK,避免把可能变化的链上地址写死到书稿里。

阅读补充

DeepBook 对象 ID 与网络强绑定。mainnet 的 Pool、Registry、BalanceManager 不能拿到 testnet RPC 上查询;即使对象 ID 格式合法,RPC 返回的“不存在”也可能只是网络选错,而不是对象被删除。

示例脚本建议总是从 RPC_URLNETWORKPOOL_ID 读取环境变量,并在输出里打印网络和对象 ID。后续涉及 package version 的章节,还要把 Registry/Pool 的版本开关纳入检查。

落地判断

  • 每个命令示例都显式列出依赖的网络和对象 ID。
  • 查询失败时先核对 RPC 网络,再排查对象权限或字段解析。
  • 不要把文档中的对象 ID 当作永久常量,升级和部署会改变入口地址。

读完以后问自己

  • mainnet Pool ID 用 testnet RPC 查询会出现什么现象?
  • 示例环境至少应该暴露哪些变量?
  • 为什么版本开关也属于网络使用检查的一部分?

ch01-09 环境安装

返回本章

先看问题

这一节不急着进源码。先用“环境安装”回答一个更基础的问题:如果要构建真实交易应用,哪些概念必须先讲清楚,哪些细节可以等到后面再拆。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

最小开发环境包括:

  • Sui CLI:执行 sui clientsui move buildsui move test
  • Node.js 和 pnpm:运行 TypeScript SDK 示例,优先用 pnpm tsx
  • Rust:构建或阅读 crates/indexercrates/server
  • PostgreSQL:运行 Indexer 或本地事件表实验。

本章示例 book/ch01/code/s01-env-check 给出环境检查命令。检查通过不代表交易一定能成功;还需要 RPC、gas coin、对象 ID 和网络一致。

阅读补充

环境检查的目的不是追求工具越多越好,而是覆盖四类任务:Move 构建测试、TypeScript 交易构造、Rust 服务阅读/运行、PostgreSQL 事件表实验。缺任何一类,后续章节都可以先跳过对应练习,但要在笔记里标注不可执行的原因。

安装后优先运行只读命令,例如 sui client active-envsui move buildnode --versionrustc --version,再进入会发交易或写数据库的练习。这样可以把环境问题和协议逻辑问题分开。

落地判断

  • 把环境检查输出保存到本章练习记录,便于后续复现。
  • 先做只读检查,再做链上交易或本地数据库写入。
  • Sui CLI 和 Move.toml edition 不匹配时,优先排查工具链版本。

读完以后问自己

  • 本章环境检查覆盖哪四类工具?
  • 为什么通过环境检查不代表交易一定能成功?
  • Move 构建失败时应先看 CLI 版本还是前端代码?

ch01-10 检查 DeepBook 源码仓库结构

返回本章

先看问题

先把“检查 DeepBook 源码仓库结构”放到读者路径里看:你不是在背一个协议名,而是在建立一套判断 DeepBook 能做什么、不能做什么的坐标。读这一节时,重点看产品边界如何落到对象、交易和数据系统上。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

当前源码仓库的顶层重点目录:

本章示例 book/ch01/code/s02-repo-map 给出生成模块清单的方法。

阅读补充

第一次检查仓库结构时,不要急着打开所有文件。先确认包目录、服务目录和脚本目录的边界,再把 README 中的模块名映射到 sources 下的具体 Move 文件。这样后续引用源码时能少走很多搜索弯路。

如果本地仓库与书稿列出的结构不同,先检查分支和版本,再更新个人笔记。不要直接修改 ch01 的源码地图去适配本机临时状态,除非这是明确的文档维护任务。

落地判断

  • 用目录层级先分产品,再用文件名分模块职责。
  • 读到陌生模块时先回 README 或 Move.toml 找包级说明。
  • 脚本目录只说明运维和调用方式,不等于协议状态定义。

读完以后问自己

  • packages/deepbookcrates/indexer 的职责差异是什么?
  • 为什么脚本目录不能作为协议真相来源?
  • 源码结构变动时应先检查什么?

ch01-11 本书项目目录规范

返回本章

先看问题

这一节不急着进源码。先用“本书项目目录规范”回答一个更基础的问题:如果要构建真实交易应用,哪些概念必须先讲清楚,哪些细节可以等到后面再拆。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

书稿正文放在 book/chXX/README.md。每章示例放在同章 code/ 下,目录名使用 sNN-topic。示例 README 必须写清依赖、环境变量和运行方式。

Move 示例必须给出 sui move buildsui move test。TypeScript 示例优先给出 pnpm installpnpm tsx。Indexer 示例必须说明 PostgreSQL、环境变量和 API 请求响应。

本章只创建 README 级示例,后续章节可以把 README 中的命令扩展成实际脚本。

阅读补充

书稿目录服务于学习路径,不服务于上游协议构建。sections 负责解释,code 负责可运行或可复现的练习,README 负责本章地图。引用上游源码时写绝对路径,是为了让读者明确自己正在读哪个仓库。

新增练习时优先放在对应章节的 code/sXX-* 目录,并在小节中说明它验证了哪条交易路径或源码概念。不要把一次性笔记散落在章节根目录,否则后续很难维护。

落地判断

  • 每个小节只解释一个主题,跨章节概念用链接或后续章节入口承接。
  • 练习代码目录名要能表达任务,不只写 demo。
  • 引用上游源码时保留绝对路径,引用本书练习时也说明章节位置。

读完以后问自己

  • sectionscode、章节 README 分别承担什么职责?
  • 新增练习为什么要放进 code/sXX-*
  • 书稿仓库和 DeepBook 源码仓库为什么要分开?

ch01-12 第一个读者任务:查询池子对象

返回本章

先看问题

先把“第一个读者任务:查询池子对象”放到读者路径里看:你不是在背一个协议名,而是在建立一套判断 DeepBook 能做什么、不能做什么的坐标。读这一节时,重点看产品边界如何落到对象、交易和数据系统上。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

选择一个真实 DeepBook 池子对象 ID,设置为 POOL_ID,然后使用 Sui CLI 或 TypeScript SDK 查询对象。最小目标不是下单,而是确认对象存在、网络正确、字段能被读取。

CLI 方向:

sui client object "$POOL_ID" --json

TypeScript 方向使用 @mysten/sui/clientgetObject,传入 showTypeshowContent。如果返回 object not found,先检查 NETWORKRPC_URL,再检查池子 ID 是否属于同一网络。

阅读补充

第一个任务选择查询 Pool,是因为它不需要签名交易,却能验证 RPC、对象 ID、package 类型和字段解析。查询结果里重点看对象类型是否包含 Pool<BaseAsset, QuoteAsset>,字段中是否能看到 bookstatevault 或版本相关结构。

读查询输出时不要只看“有返回”。要把返回类型与 pool.move 中的 Pool 定义对照,把对象 ID 与 Registry 或官方配置对照,把网络与 RPC URL 对照。三者一致,后续才适合进入下单练习。

落地判断

  • 查询脚本输出必须包含 RPC、Pool ID 和对象类型。
  • 对象存在但类型不匹配时,优先检查是否拿错池或网络。
  • 只读查询不能证明余额和权限足够,只能证明对象入口可达。

读完以后问自己

  • 查询 Pool 对象可以验证哪三类配置?
  • 对象类型里的 base/quote 泛型为什么重要?
  • 为什么第一步不直接发送下单交易?

ch01-13 术语表

返回本章

先看问题

这一节不急着进源码。先用“术语表”回答一个更基础的问题:如果要构建真实交易应用,哪些概念必须先讲清楚,哪些细节可以等到后面再拆。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

pool:某个 BaseAsset/QuoteAsset 交易对的链上池子,对应 Pool<BaseAsset, QuoteAsset>

base:交易对基础资产。SUI/USDC 中 SUI 是 base。

quote:计价资产。SUI/USDC 中 USDC 是 quote。

lot:数量离散单位。订单数量必须满足最小数量和 lot size 约束。

tick:价格离散单位。限价单价格必须满足 tick size 约束。

order id:订单唯一编号,后续在 book/order.movebook/order_info.move 分析。

checkpoint:Sui 链上全局进度点,Indexer 用它确定事件和交易处理进度。

阅读补充

术语表不是翻译表,而是防止同一个词在不同层被误用。例如 account 在钱包层、BalanceManager 层、state/account.move 层含义不同;order 在前端展示、book/order.move 压缩状态和 order_info.move 生命周期对象里也不是同一个结构。

维护术语时建议写“术语 -> 源码位置 -> 在交易路径中的职责”。只写中文解释会在后续章节失去约束力,尤其是 fee、rebate、stake、settled、owed 这类容易被产品文案简化的词。

落地判断

  • 每个核心术语都尽量附一个源码文件或后续章节入口。
  • 发现同名概念跨层含义不同,要在术语表里拆分。
  • 不要用业务口号替代对象、函数或事件的准确名称。

读完以后问自己

  • account 在 DeepBook 文档里可能指哪几层?
  • 为什么 OrderOrderInfo 不能混用?
  • 术语表如何帮助调试交易失败?

ch01-14 本章代码

返回本章

先看问题

先把“本章代码”放到读者路径里看:你不是在背一个协议名,而是在建立一套判断 DeepBook 能做什么、不能做什么的坐标。读这一节时,重点看产品边界如何落到对象、交易和数据系统上。

本节坐标

本节先把源码范围缩到最少。读的时候只抓产品边界、对象名称、函数入口和事件线索;后面进入交易路径时,再回到这些入口核对细节。

这一组入口用来校准产品边界:先看它们暴露哪些对象和动作,再判断本节概念最终会落到哪条交易或数据路径。

建立直觉

本章示例目录:

  • book/ch01/code/s01-env-check
  • book/ch01/code/s02-repo-map
  • book/ch01/code/s03-first-query

每个示例先以 README 形式给出命令和扩展方向。后续章节可以把这些 README 拆成可执行脚本。

阅读补充

本章代码都应保持“低风险、可重复、只读优先”。环境检查验证工具,repo map 验证源码路径,first query 验证链上对象入口。三者通过后,读者才具备继续进入 Move 基础和架构调用链的最低上下文。

如果要扩展本章代码,优先添加输出字段和错误提示,而不是加入复杂交易。第一章的代码目标是让读者知道自己连到了哪里、读的是哪个对象、后续应该打开哪份源码。

落地判断

  • 示例 README 要写明输入变量和预期输出,不只给命令。
  • 错误提示优先区分工具缺失、网络不通、对象不存在和类型不匹配。
  • 本章代码不承担下单逻辑,避免过早引入权限和资金风险。

读完以后问自己

  • 三个示例目录分别验证什么?
  • 为什么本章代码应只读优先?
  • first query 成功后,下一步应该打开哪个源码文件?

ch02 Sui Move 开发基础

本章目标

读完本章后,你应该能够:

  • 读懂 DeepBook 源码中的 modulestructfun、ability 和泛型。
  • 解释 keystorecopydrop 对金融协议对象的影响。
  • 区分 owned object、shared object、immutable object,以及 UIDIDTxContextClock 的职责。
  • 理解 Coin<T>Balance<T> 的转换意义。
  • 找到 event::emit 和 abort code,并知道 Indexer 为什么依赖事件。

本章学习阶梯

  • L1 用 hello_move.move 读懂 Move 基本语法。
  • L2 用 Coin<T>Balance<T>、ability 和 shared object 理解金融对象。
  • L3 从 DeepBook 源码中定位事件、abort code、泛型资产和动态字段。
  • L4 写一个最小 Move 练习,把语法变成可运行对象和事件。

源码地图

主题GitHub 源码阅读重点
Move 包配置packages/deepbook/Move.tomlpackage 名称、edition、依赖和地址映射
Move 基础示例packages/deepbook/sources/hello_move.movemodule、struct、ability、泛型、对象、事件
资金账户packages/deepbook/sources/balance_manager.moveCoin<T> 入账为 Balance<T>,余额事件,cap 和 proof
交易入口packages/deepbook/sources/pool.movePool<BaseAsset, QuoteAsset>Clock、泛型资产
订单事件packages/deepbook/sources/book/order_info.move订单生命周期事件
共享状态辅助packages/deepbook/sources/helper/big_vector.movekey 对象和动态扩展数据结构

小节目录

Move 高阶穿插点

  • 学 Move 不要停在语法表,直接用 DeepBook 的资产、订单、cap、proof 来理解 ability 的工程后果。
  • 看到 Coin<T>Balance<T> 时,追踪它们在函数边界上的转换,不要把钱包资产和协议内部余额混成一个概念。
  • assert! 时把 abort code 写成“用户会看到什么错误、开发者该回到哪条交易前检查”。

常见错误

  • 给资产、cap、proof 加上不该有的 copydrop,破坏资源约束。
  • ID 当成 UID 使用。ID 是可记录的值,UID 是对象身份字段。
  • 忘记 TxContext,导致无法 object::new(ctx)into_coin(ctx)
  • 在 TypeScript move call 中漏写泛型资产类型。
  • 只查询对象当前字段,不查询事件,导致订单历史和余额流水不完整。

本章检查清单

  • 能解释 Greeting has copy, drop, storeUserProfile has key 的差异。
  • 能说出 Coin<T> 存入 BalanceManager 后如何变成 Balance<T>
  • 能找到 BalanceEventOrderPlaced 的事件定义。
  • 能根据 abort code 回到源码定位 assert!
  • 能读懂 packages/deepbook/Move.toml

进阶练习

  1. 实现一个最小共享对象计数器:Counter has key,包含 increment(&mut Counter)value(&Counter): u64
  2. 实现一个 Coin<T> 存取款示例:存入时 into_balance,取出时 into_coin
  3. 给计数器增加事件 CounterUpdated,然后用 RPC 查询事件。

ch02-01 module、struct、fun、ability

返回本章

先建立手感

这一节用“module、struct、fun、ability”训练 Move 手感:先看对象和资源能不能被复制、丢弃、转移,再回到 DeepBook 里判断为什么这些限制有实际价值。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

Move 的 module 是代码和类型的命名空间。DeepBook 的基础示例模块是 packages/deepbook/sources/hello_move.move,开头声明 module deepbook::hello_move;。这说明模块地址来自 Move.tomldeepbook = "0x0" 映射,发布时会被实际 package ID 替换。

struct 定义数据形状和 ability。hello_move.move 中的 Greeting has copy, drop, store 是普通值;UserProfile has key 是链上对象;NonCopyableResource has store 不能随意复制或丢弃,需要显式消费。

Move 技巧:读金融协议里的 struct,第一眼不要看字段,先看 ability。没有 copy 的资源不能被复制,没有 drop 的资源不能随手丢弃,key 表示它会成为链上对象。这些约束比字段名更早决定安全边界。

fun 定义行为。new_greeting(custom_message: vector<u8>): Greeting 返回普通结构;create_user(admin_cap, name, initial_balance, ctx): UserProfile 创建对象并发出事件;mint_resource(value, ctx) 创建对象后 transfer::transfer 给交易发送者。

阅读补充

读 DeepBook 源码时,先把 module 当作命名空间和权限边界,把 struct 当作链上状态或事件形状,把 fun 当作状态迁移入口。ability 决定资源能否复制、丢弃、存储或成为对象,是金融协议最容易踩错的语法点。

建议先用 hello_move.move 建立最小语法样本,再打开 balance_manager.move 对照真实业务结构。这样能看到教学示例与生产代码之间的差异,而不是直接被完整协议的泛型和 cap 设计淹没。

Move 判断

  • 读源码先标注 module 名、公开函数和关键 struct ability。
  • 看到 has key 时立刻判断它是不是链上对象。
  • 看到无 copy/drop 的资源时,不要假设它能随意传值或销毁。

练习问题

  • modulestructfun 在 DeepBook 源码里分别对应什么阅读对象?
  • 为什么 ability 是金融对象安全的一部分?
  • hello_move.movebalance_manager.move 适合怎样配合阅读?

ch02-02 ability 对金融对象的影响

返回本章

先建立手感

先不要把“ability 对金融对象的影响”当成孤立语法点。DeepBook 里每个资产、订单和权限对象都会受 Move 类型系统约束,读这一节时要看语法如何变成资金安全边界。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

copy 表示值可以复制。金融资产、订单所有权、权限 cap 通常不能随意 copy,否则会产生重复花费或重复授权风险。

drop 表示值可以被丢弃。需要强制结算或归还的资源通常不能 drop。后续闪电贷中的 hot potato 模式就依赖不能随意丢弃的资源约束。

store 表示值可以存入其他结构。BalanceManager 中把不同资产余额放入 Bag,依赖可存储的内部值。

key 表示这是链上对象。packages/deepbook/sources/balance_manager.moveBalanceManager has key, store,说明它既是对象,也可以作为 shared object 参与 DeepBook 交易。

阅读补充

金融协议里 ability 不是语法装饰。资产余额、管理员 cap、交易 cap、临时 proof 是否能复制或丢弃,直接影响权限能否被滥用、资源能否被意外销毁、对象能否进入全局存储。

阅读时建议把每个 struct has ... 单独抄出来,旁边标注“谁创建、谁持有、能否复制、能否丢弃、能否存储”。这个表比单纯记 ability 定义更接近真实审计方法。

Move 判断

  • cap 通常不应具备 copy,否则权限可被复制扩散。
  • proof 如果可丢弃,要确认它只是临时证明而非资产本身。
  • store 允许嵌入其它对象或集合,要检查嵌入后谁还能访问。

练习问题

  • 为什么 TradeCap 不能像普通值一样复制?
  • drop 对临时 proof 和真实资产的风险有什么差异?
  • 审计一个新 struct ability 时要问哪几个问题?

ch02-03 Sui 对象:owned、shared、immutable

返回本章

先建立手感

这一节用“Sui 对象:owned、shared、immutable”训练 Move 手感:先看对象和资源能不能被复制、丢弃、转移,再回到 DeepBook 里判断为什么这些限制有实际价值。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

owned object 由一个地址拥有,例如普通 Coin<T>、权限 cap、某些用户对象。它适合用户独占操作,但高频使用时可能遇到并发和锁定问题。

shared object 可被多人在交易中引用。DeepBook 的 PoolBalanceManager 都是需要重点理解的共享状态。Pool 定义在 packages/deepbook/sources/pool.moveBalanceManager 定义在 packages/deepbook/sources/balance_manager.move

immutable object 发布后只读。Move package 本身可按不可变依赖理解。应用构造 PTB 时必须知道对象类型,因为 owned object 需要作为输入消耗或借用,shared object 需要共享对象引用,immutable package 通过函数 target 引用。

阅读补充

DeepBook 中最常见的 shared object 是 Pool 和 Registry 相关状态,常见 owned object 是用户持有的 cap 或 BalanceManager 管理入口。immutable package 则提供代码和类型定义,交易调用时通过 package/module/function 定位。

判断对象类别时,除了看定义中的 has key,还要看创建后是 transfer 给地址、share 成共享对象,还是作为 package 发布后的不可变代码存在。不同类别决定交易是否需要所有权、是否会形成共享对象并发边界。

Move 判断

  • 写交易参数时把 owned、shared、immutable 分组列出。
  • shared object 需要考虑版本和并发访问,owned object 需要考虑签名者和所有权。
  • 不要把 package ID 当成普通对象余额来查询。

练习问题

  • Pool 和 BalanceManager 在访问模式上有什么差异?
  • 为什么 shared object 更容易成为并发边界?
  • package ID 与普通 owned object 有什么不同?

ch02-04 UID、ID、TxContext、Clock

返回本章

先建立手感

先不要把“UID、ID、TxContext、Clock”当成孤立语法点。DeepBook 里每个资产、订单和权限对象都会受 Move 类型系统约束,读这一节时要看语法如何变成资金安全边界。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

UID 是对象身份字段,只能在对象内部持有。hello_move.moveUserProfileAdminCapMyResource 都包含 id: UID

ID 是对象 ID 的值形式,可复制、存储、作为事件字段。hello_move.moveUserCreated 事件包含 user_id: ID,通过 object::id(&user) 生成。

TxContext 提供交易上下文。源码中常见 object::new(ctx) 创建对象,tx_context::sender(ctx)ctx.sender() 读取发送者,ctx.timestamp_ms() 读取时间戳。

Clock 是共享时钟对象。DeepBook 交易入口 packages/deepbook/sources/pool.moveplace_limit_orderplace_market_order 都接收 clock: &Clock,用于过期时间、订单状态和交易时间判断。

阅读补充

UID 是对象身份字段,通常只在对象 struct 内部持有;ID 是可复制记录的对象标识,适合写入事件或其它状态。TxContext 提供创建对象和发送者上下文,Clock 则为订单过期、历史统计和 epoch 相关逻辑提供链上时间。

阅读交易入口时,把 ctx: &mut TxContextclock: &Clock 分开理解:前者服务对象创建和交易上下文,后者服务时间判断。把二者混用会导致你误判函数是否依赖当前时间。

Move 判断

  • 创建对象必须追踪 object::new(ctx) 的调用位置。
  • 事件里通常记录 ID 或地址,不应暴露或移动对象的 UID
  • 涉及过期时间、epoch 或历史统计时查找 Clock 参数。

练习问题

  • 为什么 UID 不能当作普通字段随意复制?
  • TxContextClock 在下单入口中分别解决什么问题?
  • 订单过期逻辑应该优先搜索哪个参数或模块?

ch02-05 泛型资产类型与 phantom

返回本章

先建立手感

这一节用“泛型资产类型与 phantom”训练 Move 手感:先看对象和资源能不能被复制、丢弃、转移,再回到 DeepBook 里判断为什么这些限制有实际价值。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

DeepBook 使用泛型资产类型表达交易对。pool.movePool<phantom BaseAsset, phantom QuoteAsset>PoolInner<phantom BaseAsset, phantom QuoteAsset> 通过类型参数区分交易池,不需要在结构体字段中实际保存 BaseAssetQuoteAsset 值。

phantom 表示类型参数只参与类型标识,不参与字段存储约束。这样 Pool<SUI, USDC>Pool<DEEP, SUI> 是不同类型,编译器能帮助防止把错误资产传进错误池子。

实践中,TypeScript 构造 move call 时也要写完整泛型类型参数。泛型错了,即使对象 ID 看起来正确,也会在类型检查或执行阶段失败。

阅读补充

DeepBook 用泛型类型表示资产对,而不是把资产地址当普通字符串字段。Pool<BaseAsset, QuoteAsset> 的类型本身就是市场身份的一部分,调用 move function 时漏写或写错 type arguments,会让交易指向完全不同的类型空间。

phantom 适合表达“类型参与静态区分,但不以值的形式存储”。阅读资产相关 struct 时,注意它是否真正持有 Coin<T>/Balance<T>,还是只用类型参数约束市场或权限。

Move 判断

  • TypeScript move call 必须显式核对 base/quote type arguments 顺序。
  • 创建池或查询池时不要只看对象 ID,也要看对象类型里的泛型。
  • 看到 phantom 时检查它是否用于类型隔离而不是运行时字段。

练习问题

  • 为什么 SUI/USDC 和 USDC/SUI 不是同一个泛型池?
  • 漏写 type arguments 会导致哪类问题?
  • phantom 资产参数和真实 Balance<T> 持有有什么区别?

ch02-06 Coin<T> 与 Balance<T>

返回本章

先建立手感

先不要把“Coin<T> 与 Balance<T>”当成孤立语法点。DeepBook 里每个资产、订单和权限对象都会受 Move 类型系统约束,读这一节时要看语法如何变成资金安全边界。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

Coin<T> 是可作为对象输入和输出的钱币对象。钱包里看到的 SUI 或 token 通常是 Coin<T> 对象。

Balance<T> 是合约内部保存的余额值,不能直接当作钱包对象传输。DeepBook 的 BalanceManager 会把用户存入的 Coin<T> 转成内部 Balance<T>,交易结算时再按需要转回 Coin<T>

源码在 packages/deepbook/sources/balance_manager.movedeposit<T> 接收 coin: Coin<T>,调用 coin.into_balance() 后进入内部余额;withdraw<T> 调用 withdraw_with_proof(...).into_coin(ctx) 返回 Coin<T>

阅读补充

Coin<T> 是可在用户钱包中持有和转移的对象形态,Balance<T> 更适合协议内部合并、拆分和记账。DeepBook 的 BalanceManager 会把用户存入的 coin 转成内部余额,交易结算时再通过 Vault 与账户余额做净额变化。

调试“余额不见了”时,先判断资产处于钱包 coin、BalanceManager 余额、Pool Vault 余额还是 open order 锁定状态。它们都是资产路径的一部分,但查询方式和权限完全不同。

Move 判断

  • 存款路径关注 into_balance,取款路径关注 into_coin(ctx)
  • 不要用钱包 coin 余额直接判断能否下 DeepBook 订单。
  • 结算问题要同时查看 BalanceManager 和 Vault 的差额处理。

练习问题

  • 为什么存入 BalanceManager 后钱包 coin 数量会变化?
  • Balance<T> 为什么更适合协议内部记账?
  • 下单前应该检查钱包余额还是 BalanceManager 余额?

ch02-07 split、join、into_balance、into_coin

返回本章

先建立手感

这一节用“split、join、into_balance、into_coin”训练 Move 手感:先看对象和资源能不能被复制、丢弃、转移,再回到 DeepBook 里判断为什么这些限制有实际价值。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

split 用于把一个 coin 拆成指定金额的小 coin。交易前常用于准备 gas 之外的输入资产。

join 用于合并同类型 coin。交易前整理碎片 coin,可以降低 PTB 输入复杂度。

into_balanceCoin<T> 消费成 Balance<T>,适合进入合约内部会计。balance_manager.movedeposit<T> 正是这个路径。

into_coinBalance<T> 变回 Coin<T>,需要 TxContext 创建新的对象身份。withdraw<T>withdraw_all<T> 使用这个路径把内部余额还给调用方。

资金流转实践:任何 Coin<T>Balance<T> 的转换都应伴随事件或状态更新,否则前端和 Indexer 难以解释余额变化。DeepBook 在 depositwithdraw 中都会发出 BalanceEvent

阅读补充

资产操作的阅读顺序建议是:split 代表从一份余额里切出数量,join 代表合并回同类余额,into_balance 把 coin 对象变成内部余额,into_coin(ctx) 则需要交易上下文重新创建 coin 对象。DeepBook 的存取款和 Vault 结算会反复使用这组动作。

不要把 split/join 当作普通数字加减。它们操作的是资源,失败或遗漏会造成资源守恒问题,Move 类型系统会帮助约束,但业务逻辑仍要检查数量、资产类型和授权。

Move 判断

  • 每次 split 都要能说明切出的资产去向。
  • 每次 join 都要确认资产类型一致且不会绕过费用或锁定逻辑。
  • Balance<T> 回到 Coin<T> 时确认函数是否需要 TxContext

练习问题

  • into_coin(ctx) 为什么需要 TxContext
  • Vault 结算中 split 和 join 分别对应哪些资产方向?
  • 为什么资产操作不能等同于普通整数运算?

ch02-08 动态字段

返回本章

先建立手感

先不要把“动态字段”当成孤立语法点。DeepBook 里每个资产、订单和权限对象都会受 Move 类型系统约束,读这一节时要看语法如何变成资金安全边界。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

动态字段适合在对象下挂载规模不固定的数据。订单簿、账户映射、注册表和余额容器都可能需要这种模式。

balance_manager.move 使用 Bag 保存不同资产余额,并定义 BalanceKey<phantom T> 作为资产余额的 key。源码还引入 dynamic_field as df,用于 referral 等对象下字段。

registry.move 使用动态字段和 key 管理池子、稳定币和应用授权。后续读注册表时要关注 key 类型,而不是只搜索字符串。

阅读补充

动态字段适合把“按类型或 key 扩展”的状态挂在对象下面。DeepBook 中授权、余额或大集合辅助结构会使用这种模式,避免在主对象 struct 中预先写死所有可能字段。

阅读动态字段时,重点找 key 类型、父对象、增删改入口和是否存在反向索引。只知道用了 dynamic field 还不够,调试时必须能从父对象 ID 和 key 还原查询路径。

Move 判断

  • 标注每个动态字段的父对象和 key 类型。
  • 写查询工具时不要只拿对象字段,还要考虑动态字段分页。
  • 删除授权或余额字段时检查是否同步清理相关索引或事件。

练习问题

  • 动态字段解决 DeepBook 哪类扩展问题?
  • 查询动态字段需要知道哪两个核心信息?
  • 为什么动态字段会影响 Indexer 设计?

ch02-09 event::emit 与 Indexer

返回本章

先建立手感

这一节用“event::emit 与 Indexer”训练 Move 手感:先看对象和资源能不能被复制、丢弃、转移,再回到 DeepBook 里判断为什么这些限制有实际价值。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

链上状态告诉你“现在是什么”,事件告诉你“发生过什么”。Indexer 依赖事件重建订单、成交、余额、治理等时间序列。

hello_move.movecreate_user 发出 UserCreatedsend_greeting 发出 GreetingSentupdate_balance 发出 BalanceUpdated。这是学习事件的最小样本。

DeepBook 真实事件更多:balance_manager.move 发出 BalanceManagerEventBalanceEventbook/order_info.move 发出 OrderPlacedOrderFilledOrderFullyFilled 等;book/order.move 发出 OrderCanceledOrderModified

阅读补充

事件是交易执行后的链上事实记录,Indexer 把这些事实转换成查询友好的表或 API。DeepBook 的订单状态、成交、余额变化和费用解释,都不能只靠当前对象字段复原完整历史。

阅读事件时先找 event::emit,再看事件 struct 字段,最后看 indexer 如何消费这些字段。字段少了会影响链下可观测性,字段语义错了会让前端和报表都误读交易。

Move 判断

  • 每个用户可见状态变化都要能找到对应事件或解释为什么没有事件。
  • Indexer 延迟不等于链上交易失败,前端应区分 pending indexing 和 failed execution。
  • 事件字段应包含足够的对象 ID、账户 ID 和数量信息,方便回放。

练习问题

  • 为什么当前对象字段不能替代订单历史?
  • event::emit 到 Indexer 表之间有哪些转换步骤?
  • 前端订单列表缺一笔成交时应先查 digest 还是数据库?

ch02-10 abort code

返回本章

先建立手感

先不要把“abort code”当成孤立语法点。DeepBook 里每个资产、订单和权限对象都会受 Move 类型系统约束,读这一节时要看语法如何变成资金安全边界。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

Move 中 assert!(condition, code) 失败会 abort。DeepBook 源码通常在模块顶部定义错误码,例如 balance_manager.moveEInvalidOwnerEInvalidTraderEBalanceManagerBalanceTooLowpool.moveEInvalidFeeEInvalidTickSizeEMinimumQuantityOutNotMet

读 abort 的方法:

  1. 先看交易错误中的 module 和 code。
  2. 打开对应GitHub 源码文件,搜索 const E
  3. 找到触发 assert! 的函数,理解参数和状态前置条件。

生产应用不能把 abort code 原样丢给用户。应该映射成“余额不足”“订单价格不满足 tick size”“池子版本不可用”等可操作提示。

阅读补充

abort code 是交易失败后回到源码的索引。遇到失败时,不要只记录错误文本,要定位 module、code 和触发的 assert! 条件,再把它翻译成用户能理解的原因,例如余额不足、版本禁用、价格粒度错误或权限证明无效。

DeepBook 的失败通常不是单点原因。一个下单交易可能先过 Registry/Pool 版本检查,再过 tick/lot/min size,再过 BalanceManager proof 和 Vault 余额检查。阅读 abort 时要沿调用链逐层排除。

Move 判断

  • 日志里保留 module、abort code、交易 digest 和输入参数。
  • 把常见 abort 映射到用户提示,但不要吞掉原始 code。
  • 排查时按调用链顺序查 assert!,避免只看最后一个模块。

练习问题

  • abort code 如何帮助定位 assert!
  • 价格粒度错误和权限错误在排查顺序上有什么不同?
  • 用户提示为什么不能只写“交易失败”?

ch02-11 Move 单元测试和 test_scenario

返回本章

先建立手感

这一节用“Move 单元测试和 test_scenario”训练 Move 手感:先看对象和资源能不能被复制、丢弃、转移,再回到 DeepBook 里判断为什么这些限制有实际价值。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

Move 单元测试通常放在同包源码或测试模块中,使用 #[test]#[test_only] 标注。hello_move.move 末尾提供 #[test_only] public fun test_helper_function(): u64,说明测试辅助函数可以只在测试环境可见。

Sui 对象交互测试常用 sui::test_scenario 模拟多交易、多地址、多对象流转。ch02 示例 book/ch02/code/s01-hello-move 会给出最小包的 build/test 命令,后续章节再扩展到场景测试。

实践中,每次修改 Move 示例后先运行:

sui move build
sui move test

阅读补充

Move 单元测试适合验证纯逻辑、资源约束和 abort 条件;test_scenario 适合模拟多交易、多地址和对象所有权变化。DeepBook 完整下单依赖多个共享对象和资产状态,测试时应先拆成更小的模块行为。

写测试时不要只覆盖成功路径。金融协议文档中的检查问题最好都能变成一个 abort 测试:错误权限、错误资产类型、数量不满足 lot size、版本关闭等。

Move 判断

  • 先测小模块资源约束,再组合交易场景。
  • 每个 abort 分支至少保留一个可读测试名称。
  • 多地址场景要显式切换 sender,避免误判所有权。

练习问题

  • 普通 Move 单元测试和 test_scenario 各适合什么问题?
  • DeepBook 下单为什么不适合一开始就写成超大集成测试?
  • 哪些文档检查问题可以转成 abort 测试?

ch02-12 阅读 Move.toml

返回本章

先建立手感

先不要把“阅读 Move.toml”当成孤立语法点。DeepBook 里每个资产、订单和权限对象都会受 Move 类型系统约束,读这一节时要看语法如何变成资金安全边界。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

DeepBookV3 的包配置在 packages/deepbook/Move.toml

[package]
name = "deepbook"
edition = "2024.beta"
version = "0.0.1"

[dependencies]
token = { git = "https://github.com/MystenLabs/deepbookv3.git", subdir = "packages/token", rev = "main"}

[addresses]
deepbook = "0x0"

name 决定包名,edition 决定 Move 语法版本,dependencies 说明依赖 token 包,addresses 给源码中的 deepbook::... 提供编译期地址占位。

开发实践:本地练习包可以先用 0x0,发布后 CLI 会给出真实 package ID。调用链上函数时必须使用真实 package ID,而不是 0x0

阅读补充

Move.toml 是进入一个 Move 包的第一张地图。它告诉你包名、edition、依赖来源和地址别名;这些信息会影响源码里的 module 路径、发布地址和测试构建行为。

比较 DeepBook、Margin、Predict 的 Move.toml,可以快速看出它们是独立包还是共享依赖。不要只凭目录名判断协议关系,包配置里的依赖才是更可靠的入口。

Move 判断

  • 读源码前先确认当前包名和地址别名。
  • 升级或切换分支后重新检查 dependency rev/path。
  • 引用模块时用 Move.toml 中的地址映射解释来源。

练习问题

  • Move.toml 中哪些字段会影响源码阅读?
  • 为什么包依赖比产品名称更能说明边界?
  • 地址别名写错会造成哪类调用问题?

ch02-13 本章代码

返回本章

先建立手感

这一节用“本章代码”训练 Move 手感:先看对象和资源能不能被复制、丢弃、转移,再回到 DeepBook 里判断为什么这些限制有实际价值。

Move 对照

先用下面几处源码建立 Move 概念的落点。这里不追完整协议流程,只确认类型、ability、对象、事件和测试入口如何在真实项目中出现。

读这些文件时,把语法点和真实对象放在一起看:ability、泛型、对象 ID、事件和测试入口分别承担什么约束。

拆开来看

本章示例目录:

  • book/ch02/code/s01-hello-move
  • book/ch02/code/s02-coin-balance
  • book/ch02/code/s03-shared-object
  • book/ch02/code/s04-event-indexing

阅读补充

本章代码的目标是把 Move 基础语法压缩成可执行的小实验。hello-move 负责语法和 ability,coin-balance 负责资产转换,shared-object 负责对象访问模式,event-indexing 负责链上事件到链下读模型。

这些练习不需要复刻 DeepBook 全量逻辑,但每个练习都应该能指向 DeepBook 中的真实源码位置。这样读者写完小例子后,能自然迁移到 balance_manager.movepool.moveorder_info.move

Move 判断

  • 每个练习 README 都写明对应的 DeepBook 源码文件。
  • 练习代码优先验证一个 Move 概念,不混入完整交易所流程。
  • 事件练习要同时给出 emit 位置和查询方式。

练习问题

  • 四个代码练习分别对应哪些 Move 基础概念?
  • 为什么小练习要绑定真实 DeepBook 文件?
  • 完成本章代码后应能读懂哪些生产模块?

ch12 DeepBook SDK 集成专题

本章目标

官方 DeepBookV3 SDK 文档把 @mysten/deepbook-v3 定位为 TypeScript 集成层:它抽象交易调用、维护 coins/pools/managers 的 key-value 配置,并通过 Sui TypeScript SDK 构造和提交交易。本章在此基础上继续往工程化推进:不仅要会调用 SDK,还要知道 SDK 背后的 Move 入口、对象 ID、type arguments、dry run 和错误定位。

本章把前面章节中的 Move 合约能力落到应用工程:用 @mysten/deepbook-v3、Sui SDK 和 PTB 把 DeepBook Spot、Margin、Predict 的核心交易封装成可复用服务。目标不是堆 API 清单,而是建立一套生产应用可以采用的 SDK 边界:配置集中管理、交易只构造不托管私钥、提交前 dry run、失败后能解析错误并定位到 Move 入口。

本章代码放在 book/ch12/code/,每个目录都是可以扩展成 TypeScript 工程的骨架。

本章学习阶梯

  • L1 先安装 SDK、连接 RPC、读取配置。
  • L2 查询池子、资产、BalanceManager 和对象状态。
  • L3 封装限价单、市价单、swap、撤单、Margin 和 Predict 交易。
  • L4 加入 dry run、错误解析、钱包签名和后端安全构造交易。

关键定义卡片

SDK 封装的目标不是隐藏 Move,而是稳定翻译 Move 签名。比如 BalanceManager 操作背后是:

public fun new(ctx: &mut TxContext): BalanceManager
public fun deposit<T>(balance_manager: &mut BalanceManager, coin: Coin<T>, ctx: &mut TxContext)
public fun withdraw<T>(
    balance_manager: &mut BalanceManager,
    withdraw_amount: u64,
    ctx: &mut TxContext,
): Coin<T>
public fun generate_proof_as_owner(
    balance_manager: &BalanceManager,
    ctx: &TxContext,
): TradeProof

所以 SDK 服务层必须保存三类信息:对象 ID、类型参数、资源输入输出。只暴露 placeLimitOrder() 这种名字不够,错误发生时要能打印出 pool id、manager id、coin type、price/quantity 原始整数、dry run error 和对应 Move 入口。

源码地图

  • Sui Docs: DeepBookV3 SDK:安装、constants、DeepBookClient、BalanceManager key、pool/coin/manager 配置和示例代码。
  • scripts/config/constants.ts:主网、测试网的 admin cap、margin cap、supplier cap、market maker 地址等配置来源。
  • scripts/transactions/deepbookMarketMaker.ts:继承 DeepBookClient,封装 keypair、SuiClient、签名提交、限价单和闪电贷 PTB。
  • scripts/transactions/createPool.ts:用 SuiGrpcClient().$extend(deepbook(...)) 初始化 SDK,并调用 deepBookAdmin.createPoolAdmin
  • scripts/transactions/prepBalanceManager.ts:配置 balanceManagers,调用 balanceManager.createAndShareBalanceManager
  • scripts/transactions/marginPrep.tssupplyToMarginPool.tsenableMarginVersion.ts:Margin admin、maintainer、supplier 侧 SDK 调用。
  • scripts/utils/utils.ts:读取 signer、设置 gas payment、dryRunTransactionBlock、生成 multisig 交易 bytes。
  • packages/deepbook/sources/pool.move:Spot 交易入口,例如 place_limit_orderplace_market_orderswap_exact_base_for_quotecancel_orderborrow_flashloan_base
  • packages/deepbook/sources/balance_manager.moveBalanceManager、deposit、withdraw、cap 和 trade proof。
  • packages/deepbook_margin/sources/margin_manager.move:Margin 用户入口,例如 deposit、withdraw、borrow、repay、条件单和风险查询。
  • packages/deepbook_margin/sources/margin_pool.move:Margin 资金池 supply、withdraw、费用和利率状态。
  • packages/predict/sources/predict.movepredict_manager.moveregistry.move:Predict 当前 Move 层入口。Predict 需要按实际发布网络和对象 ID 标注,不应把迁移中的能力写成稳定主网接口。

小节目录

本章代码

  • book/ch12/code/s01-sdk-init/:SDK 初始化模板。
  • book/ch12/code/s02-deepbook-service/:Spot 服务封装。
  • book/ch12/code/s03-margin-service/:Margin 服务封装。
  • book/ch12/code/s04-predict-service/:Predict 服务封装。
  • book/ch12/code/s05-wallet-flow/:前端钱包交易流程。
  • book/ch12/code/s06-dry-run-helper/:dry run 和错误解析工具。

Move 高阶穿插点

  • SDK 集成的本质是把 Move 函数签名翻译成类型安全的交易构造器。
  • 每个 wrapper 都应保留 type arguments、object ids、coin 输入输出和 dry run 结果,方便定位失败。
  • 不要让 SDK 隐藏关键资源语义;用户至少要知道哪些对象被 mutably borrowed,哪些 coin 被消费。

常见错误

  • 只复制官方 SDK 示例,不补 dry run、错误解析、钱包签名和对象版本刷新。
  • 把主网对象 ID 用在测试网交易中。
  • 前端要求用户导出私钥,而不是调用钱包签名。
  • 忽略 BalanceManager 所有权和 cap,导致交易 dry run 通过不了。
  • 只检查 SDK 抛错字符串,不解析 effects.status.error
  • Predict 未标注网络和版本状态,把迁移中的接口写成稳定接口。
  • 管理员交易不设置 expiration 和固定 gas payment,导致 multisig 流程不可复现。

本章检查清单

  • 所有服务初始化都能明确 networkrpcUrladdress
  • 所有 cap、pool、manager、registry 对象 ID 都来自配置层。
  • 用户交易返回 PTB 或 bytes,不要求后端托管私钥。
  • 每个写交易都有 dry run 和错误解析路径。
  • Margin 交易检查 oracle、风险率和版本。
  • Predict 交易标注 package/object IDs、网络和版本状态。

进阶练习

  1. DeepBookService 增加 route swap,并在 dry run 后校验最小输出。
  2. MarginService 增加 borrowAndPlaceLimitOrder 组合 PTB。
  3. PredictService 增加 quote 缓存和 oracle stale 检查。
  4. 用 fixture 写一个 dry run 错误解析测试表,覆盖 Move abort、对象不存在、余额不足和 gas 不足。

ch12-01 SDK 章的边界

返回本章

先定封装边界

读这一节时把自己当成 SDK 维护者:封装应该暴露业务意图,隐藏重复参数,同时保留 dry run 和错误定位能力。

源码入口

  • scripts/transactions/deepbookMarketMaker.ts:继承式 DeepBookClient 初始化和交易封装。
  • scripts/transactions/createPool.tsprepBalanceManager.ts:插件式 DeepBook SDK 调用。
  • packages/predict/sources/predict.movepredict_manager.moveregistry.move:Predict 当前 raw Move 封装边界。

SDK 读法

SDK 层要做三件事:

  1. 把对象 ID、package ID、coin key、pool key、BalanceManager key 从业务代码中抽离。
  2. 把一组 Move 调用组合成 PTB,让上层只传业务参数。
  3. 在签名前完成模拟、Gas、错误解析和对象状态检查。

DeepBook SDK 有两类常见初始化路径。第一类是 DeepBookClient 类实例,适合后端 worker、做市脚本和测试脚本;scripts/transactions/deepbookMarketMaker.ts 就是这种写法。第二类是 SuiGrpcClient().$extend(deepbook(...)) 插件式 client,适合把 client.deepbook.deepBookAdminclient.deepbook.balanceManagerclient.deepbook.marginPool 分层调用;scripts/transactions/createPool.tsprepBalanceManager.ts 使用这种写法。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • 服务边界统一为配置加载、PTB 构造、dry run、错误解析和钱包签名交接。
  • Spot/Margin 优先用 SDK builder,Predict 用 raw Move target 并标注版本状态。
  • 不要在 SDK 章引入托管私钥或隐藏对象 ID 的黑盒服务。

动手检查

  • 本章哪些能力属于 SDK 封装,哪些属于后端或钱包职责?
  • Predict 为什么不能直接假设存在 Spot 同形 SDK?
  • 一个生产服务初始化最少需要哪些配置字段?

ch12-02 安装和配置

返回本章

先定封装边界

SDK 小节先看封装边界:好的服务层不是隐藏 Move,而是减少对象、精度、权限和网络配置错误,同时保留 dry run 与错误定位能力。

源码入口

  • book/ch12/code/s01-sdk-init/:SDK 初始化模板。
  • scripts/config/constants.ts:网络、cap、pool、manager 地址配置来源。
  • packages/predict/sources/*:Predict 需要额外标注 package/object IDs 和版本状态。

SDK 读法

SDK 集成要先能运行一个最小初始化脚本,再谈交易封装。建议把本章示例放进独立的 TypeScript 工作区,先固定依赖,再把网络、package ID、pool ID 和钱包签名方式分层管理。

最小依赖:

pnpm init
pnpm add @mysten/deepbook-v3 @mysten/sui
pnpm add -D typescript tsx @types/node

package.json 至少保留一个可执行入口:

{
  "type": "module",
  "scripts": {
    "check:sdk": "tsx src/check-sdk.ts"
  },
  "dependencies": {
    "@mysten/deepbook-v3": "<pin a tested version>",
    "@mysten/sui": "<pin a tested version>"
  },
  "devDependencies": {
    "tsx": "<pin a tested version>",
    "typescript": "<pin a tested version>"
  }
}

运行时配置不要混在代码里。主网、测试网、本地 sandbox 至少拆成三组环境变量:

SUI_NETWORK=mainnet
SUI_RPC_URL=https://sui-mainnet.mystenlabs.com
SUI_ADDRESS=0x...
DEEPBOOK_CONFIG_PATH=./config/mainnet.json

本地 sandbox 可以直接指向 ch16 的服务:

SUI_NETWORK=localnet
SUI_RPC_URL=http://localhost:9000
DEEPBOOK_SERVER_URL=http://localhost:9008
DEEPBOOK_MANIFEST_URL=http://localhost:9009/manifest

后端运维脚本或机器人可以使用私钥,但要和用户交易隔离:

SUI_PRIVATE_KEY=suiprivkey...
GAS_OBJECT=0x...

前端不应读取 SUI_PRIVATE_KEY。前端只构造交易,交给钱包签名;后端只有在运维脚本、做市脚本或管理员 multisig 流程中才读取私钥、keystore 或 multisig cap。

最小验证脚本不应该一上来下单。先确认 RPC、地址格式、配置文件和 SDK import 都可用:

// src/check-sdk.ts
import { SuiClient, getFullnodeUrl } from "@mysten/sui/client";

const rpcUrl = process.env.SUI_RPC_URL ?? getFullnodeUrl("testnet");
const client = new SuiClient({ url: rpcUrl });

const chain = await client.getChainIdentifier();
console.log({ rpcUrl, chain });

运行:

SUI_RPC_URL=http://localhost:9000 pnpm check:sdk

如果本地 sandbox 尚未启动,这一步应该失败在 RPC 连接,而不是失败在 DeepBook 配置。这样才能把“网络不可用”和“交易参数错误”分开排查。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • 依赖安装后先跑类型检查或最小初始化脚本,确认 SDK 版本兼容。
  • .env 或配置文件只存 RPC、network、object IDs,不存用户私钥。
  • Predict 配置缺少迁移状态时只允许示例构造,不允许真实交易。
  • sandbox 重置后重新读取 manifest,不复用旧 package ID 或 pool ID。

动手检查

  • 安装完成后如何验证 SuiClient 和 DeepBook SDK 可用?
  • 哪些配置应来自 constants.ts 风格的集中表?
  • Predict 的 package ID 和版本状态为什么必须成对出现?

ch12-03 SuiClient、network 和对象配置

返回本章

先定封装边界

读这一节时把自己当成 SDK 维护者:封装应该暴露业务意图,隐藏重复参数,同时保留 dry run 和错误定位能力。

源码入口

  • scripts/config/constants.ts:admin cap、margin cap、supplier cap、market maker 配置形态。
  • scripts/utils/utils.ts:signer、gas payment 和 dry run 工具。
  • packages/predict/sources/registry.move:Predict registry/predict object ID 的配置来源。

SDK 读法

DeepBook 交易不能把对象 ID 写散在业务函数里。配置层至少包含:

  • networkmainnettestnetdevnetlocalnet
  • rpcUrl:优先来自环境变量,默认可以回退到 Sui 官方 fullnode URL。
  • address:构造交易的发送者,也是 SDK 初始化时的 address
  • adminCapmarginAdminCapmarginMaintainerCap:管理员路径使用。
  • balanceManagers:业务账户和 BalanceManager object ID 的映射。
  • poolKeycoinKey:业务传 "SUI_DBUSDC""USDC" 这类 key,不直接拼 Move type。

scripts/config/constants.ts 中的 adminCapIDmarginAdminCapIDsupplierCapIDmarketMakerID 展示了主网和测试网配置表的形态。测试网字段为空时,服务层必须显式报错,而不是默默发送无效对象 ID。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • SuiClient 初始化显式接收 network、rpcUrl、address,不从全局隐式读取。
  • 对象配置按网络分组,mainnet/testnet/localnet 不能互相复用。
  • 读取共享对象后校验 type、owner/shared、object version 和 package 来源。

动手检查

  • 同一个 object ID 放错网络会导致什么失败?
  • 服务层何时应该抛 CONFIG_MISSING
  • Predict oracle ID 和 predict object ID 应在哪一层校验?

ch12-04 DeepBookClient 初始化

返回本章

先定封装边界

SDK 小节先看封装边界:好的服务层不是隐藏 Move,而是减少对象、精度、权限和网络配置错误,同时保留 dry run 与错误定位能力。

源码入口

  • scripts/transactions/deepbookMarketMaker.tsDeepBookClient 类实例路径。
  • scripts/transactions/createPool.tsSuiGrpcClient().$extend(deepbook(...)) 插件路径。
  • packages/deepbook/sources/pool.movebalance_manager.move:客户端最终绑定的 Move 入口。

SDK 读法

scripts/transactions/deepbookMarketMaker.ts 的核心初始化是:

const client = new DeepBookClient({
  address,
  env: "mainnet",
  client: new SuiClient({ url: getFullnodeUrl("mainnet") }),
  balanceManagers,
  adminCap,
});

继承式封装适合把签名能力和交易构造放在同一个类中。它的风险是容易让服务层托管用户私钥,所以应只用于后端自有账户。面向用户的钱包交易时,服务层应返回 Transaction 或交易 bytes,由钱包签名提交。

工程提醒:SDK 初始化不是把 RPC URL 填进去就结束。你要明确签名者是谁、交易由谁提交、BalanceManager 属于谁、哪些对象来自配置,哪些对象来自用户选择。这个边界不清,后面所有 Move 调用都会变成权限和对象错误。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • 继承式 client 适合做市脚本,插件式 client 适合按模块调用。
  • 初始化时把 signer、client、balanceManagers、pools 依赖注入服务。
  • Spot 初始化不要顺手假设 Predict 也已有同级 SDK。

动手检查

  • DeepBookClient$extend(deepbook(...)) 适用场景有什么差异?
  • BalanceManager 配置缺失会影响哪些下单方法?
  • Predict raw Move 封装为什么应放在单独服务里?

ch12-05 constants 管理主网和测试网

返回本章

先定封装边界

读这一节时把自己当成 SDK 维护者:封装应该暴露业务意图,隐藏重复参数,同时保留 dry run 和错误定位能力。

源码入口

  • scripts/config/constants.ts:主网/测试网对象 ID 表。
  • scripts/transactions/marginPrep.tsenableMarginVersion.ts:cap 配置消费方式。
  • packages/predict/sources/predict.moveregistry.move:Predict 配置应包含的对象。

SDK 读法

配置文件建议按“网络 -> 对象角色 -> object ID”组织,而不是按业务函数组织。对象角色包括:

  • DeepBook admin:创建池、版本管理、授权 Margin app。
  • Margin admin:版本开关、Pyth config、pause cap。
  • Margin maintainer:创建 margin pool、更新利率和风控参数。
  • Supplier cap:向 margin pool 供给流动性。
  • BalanceManager:交易资金账户。

读取配置时要检查三类错误:网络不存在、对象 ID 为空、对象 ID 不属于当前网络。第三类需要用 client.getObject 查询 owner/type/version,并在 dry run 前失败。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • constants.ts 按 network 分组保存 cap、pool、manager、registry、predict 对象。
  • 测试网字段为空时 fail fast,不发送空对象 ID。
  • Predict 增加 predictVersionStatus、package ID、registry ID、predict ID、oracle IDs。

动手检查

  • 哪些对象 ID 绝不能散落在业务函数里?
  • 管理员 cap 和用户 manager 配置的生命周期有什么不同?
  • 如何防止主网 cap 被测试网交易误用?

ch12-06 查询池子、资产元数据和对象状态

返回本章

先定封装边界

SDK 小节先看封装边界:好的服务层不是隐藏 Move,而是减少对象、精度、权限和网络配置错误,同时保留 dry run 与错误定位能力。

源码入口

  • packages/deepbook/sources/pool.movebalance_manager.move:Spot 对象状态。
  • packages/deepbook_margin/sources/margin_manager.movemargin_pool.move:Margin 状态。
  • packages/predict/sources/predict.moveoracle.movevault/vault.move:Predict 状态。

SDK 读法

查询层通常分成两类:

  • 链上对象查询:client.getObjectclient.multiGetObjects,用于确认 Pool、BalanceManager、MarginManager、cap 是否存在。
  • SDK 或 Move view 查询:池子参数、订单、账户余额、可交易检查。对应源码在 packages/deepbook/sources/pool.moveget_orderget_ordersaccount_open_orderslocked_balancecan_place_limit_ordercan_place_market_order

交易前检查要覆盖 pool key、BalanceManager key、订单价格和数量精度、余额、DEEP 支付策略、对象版本是否过旧。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • 查询对象时请求 content、owner、type、previousTransaction 和 version。
  • Spot/Margin/Predict 状态分别映射到 pool、manager/pool、oracle/vault/PLP。
  • 缓存只做加速,交易构造前重新校验关键共享对象版本。

动手检查

  • 查询 pool 和查询 Predict vault 需要关注的字段有什么不同?
  • 资产 decimals 错误会影响哪些服务方法?
  • 缓存对象版本过期时 dry run 可能出现什么错误?

ch12-07 BalanceManager 操作

返回本章

先定封装边界

读这一节时把自己当成 SDK 维护者:封装应该暴露业务意图,隐藏重复参数,同时保留 dry run 和错误定位能力。

源码入口

  • scripts/transactions/prepBalanceManager.ts:创建和分享 BalanceManager。
  • packages/deepbook/sources/balance_manager.move:deposit、withdraw、cap。
  • packages/predict/sources/predict_manager.move:PredictManager 内部复用 BalanceManager。

SDK 读法

BalanceManager 是 DeepBook 交易资金账户。源码在 packages/deepbook/sources/balance_manager.move,核心函数包括 newdepositwithdrawmint_trade_capgenerate_proof_as_owner。SDK 封装至少提供:

  • 创建并 share BalanceManager。
  • deposit 指定 coin。
  • withdraw 指定 coin 和数量。
  • 查询 BalanceManager 中的 coin 余额。
  • 为子账户或策略进程发放 trade cap、deposit cap、withdraw cap。

scripts/transactions/prepBalanceManager.ts 展示了 balanceManagers 映射和 createAndShareBalanceManager 调用方式。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • BalanceManager 创建、deposit、withdraw 都返回 PTB,不直接签名。
  • trade proof/cap 的获取和使用封装在服务层,不暴露给 UI 乱传。
  • 说明 PredictManager 是另一层用户对象,不能直接拿 Spot BalanceManager 当 Predict manager。

动手检查

  • 用户下单前为什么通常需要 BalanceManager?
  • cap 或 manager owner 错误会在哪个阶段暴露?
  • PredictManager 与 Spot BalanceManager 的关系和差异是什么?

ch12-08 限价单封装

返回本章

先定封装边界

SDK 小节先看封装边界:好的服务层不是隐藏 Move,而是减少对象、精度、权限和网络配置错误,同时保留 dry run 与错误定位能力。

源码入口

  • scripts/transactions/deepbookMarketMaker.ts:限价单 SDK 调用示例。
  • packages/deepbook/sources/pool.moveplace_limit_order 等入口。
  • scripts/utils/utils.ts:dry run 与 gas 设置。

关键定义

SDK wrapper 最终要构造这个 Move call:

public fun place_limit_order<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    balance_manager: &mut BalanceManager,
    trade_proof: &TradeProof,
    client_order_id: u64,
    order_type: u8,
    self_matching_option: u8,
    price: u64,
    quantity: u64,
    is_bid: bool,
    pay_with_deep: bool,
    expire_timestamp: u64,
    clock: &Clock,
    ctx: &TxContext,
): OrderInfo

因此 TypeScript wrapper 的职责不是隐藏这些参数,而是把用户输入稳定转换成它们:

  • poolKey -> Pool<BaseAsset, QuoteAsset> 和 type arguments。
  • balanceManagerKey -> BalanceManager 对象和 TradeProof 生成方式。
  • decimal price/quantity -> 链上整数,并校验 tick/lot。
  • orderType/selfMatchingOption -> DeepBook 常量。
  • expire_timestamp -> 毫秒时间戳或默认过期策略。

SDK 读法

限价单对应 packages/deepbook/sources/pool.moveplace_limit_order。SDK 层不要直接暴露 Move 参数顺序,而是暴露领域参数:

type LimitOrderInput = {
  poolKey: string;
  balanceManagerKey: string;
  clientOrderId: string;
  price: number;
  quantity: number;
  isBid: boolean;
  orderType?: "NO_RESTRICTION" | "IMMEDIATE_OR_CANCEL" | "POST_ONLY";
  selfMatchingOption?: "ALLOW" | "CANCEL_TAKER" | "CANCEL_MAKER";
  payWithDeep?: boolean;
};

提交前要调用 can_place_limit_order 或做 dry run,错误时把价格精度、数量精度、余额不足、pool 不存在和版本不允许区分开。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • 限价单请求验证 poolKey、price、quantity、isBid、expiration、clientOrderId。
  • PTB target 或 SDK builder 调用要能在单元测试中断言。
  • dry run 后检查订单对象变化、余额锁定和 Move abort。

动手检查

  • 限价单服务方法最少需要哪些参数?
  • price/quantity 精度错误会导致哪类失败?
  • 如何把 order already expired 或余额不足提示给用户?

ch12-09 市价单封装

返回本章

先定封装边界

读这一节时把自己当成 SDK 维护者:封装应该暴露业务意图,隐藏重复参数,同时保留 dry run 和错误定位能力。

源码入口

  • packages/deepbook/sources/pool.moveplace_market_order、swap 入口。
  • scripts/transactions/deepbookMarketMaker.ts:服务封装参考。
  • scripts/utils/utils.ts:dry run 后检查 balanceChanges。

SDK 读法

市价单对应 place_market_order,应用层通常以“花多少 quote 买 base”或“卖多少 base 换 quote”的方式表达。服务层要明确:

  • isBid 的含义。
  • quantity 是 base 数量还是 quote 数量。
  • 是否允许部分成交。
  • 最小成交数量或滑点保护。

对 swap 类交易,SDK 还提供 swapExactQuoteForBaseswapExactBaseForQuote 等更接近前端 swap 的方法,底层对应 pool.move 的 swap 入口。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • 市价单接口明确 exact base、exact quote 或 swap exact in/out。
  • 设置最大滑点或最小输出,并在 dry run 后校验 balanceChanges。
  • 部分成交策略必须由调用方显式选择。

动手检查

  • 市价买入时 quantity 表示 base 还是 quote?
  • 没有最小输出保护会产生什么用户风险?
  • 市价单和 swap 封装何时应使用不同方法名?

ch12-10 撤单、批量撤单和订单查询

返回本章

先定封装边界

SDK 小节先看封装边界:好的服务层不是隐藏 Move,而是减少对象、精度、权限和网络配置错误,同时保留 dry run 与错误定位能力。

源码入口

  • packages/deepbook/sources/pool.move:cancel/order query 入口。
  • scripts/transactions/deepbookMarketMaker.ts:订单服务调用模式。
  • book/ch12/code/s02-deepbook-service/:批量撤单服务骨架。

SDK 读法

撤单入口包括 cancel_ordercancel_orderscancel_live_ordercancel_live_orderscancel_all_orders。服务层应统一返回撤单 PTB,并在调用前查询订单是否属于当前 BalanceManager。订单查询可绑定 get_orderget_ordersget_account_order_detailsaccount_open_orders

批量撤单需要限制单笔 PTB 中的订单数,避免 gas 估算过高或对象读写集合过大。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • 撤单前查询订单归属、pool、orderId 和 BalanceManager。
  • 批量撤单限制单笔订单数,并对失败订单返回逐项结果。
  • 查询方法与写交易分离,避免只为读订单构造 PTB。

动手检查

  • cancel order 为什么要先确认订单属于当前 manager?
  • 批量撤单过大可能触发哪些 gas 或对象集合问题?
  • 订单已成交、已取消、不存在的提示应如何区分?

ch12-11 swap/route 封装

返回本章

先定封装边界

读这一节时把自己当成 SDK 维护者:封装应该暴露业务意图,隐藏重复参数,同时保留 dry run 和错误定位能力。

源码入口

  • packages/deepbook/sources/pool.move:swap 与 flashloan 相关入口。
  • scripts/transactions/deepbookMarketMaker.ts:borrow/swap/return PTB 示例。
  • book/ch12/code/s02-deepbook-service/:route 服务扩展位置。

SDK 读法

单池 swap 可以直接封装 SDK 的 deepBook.swapExactQuoteForBasedeepBook.swapExactBaseForQuote。多池 route 应拆成:

  1. 报价阶段:读取每个池子的 depth、tick、fee、滑点。
  2. 构造阶段:按路径串联 PTB,前一跳输出 coin 进入后一跳。
  3. 校验阶段:dry run 后检查 balanceChangesobjectChanges

scripts/transactions/deepbookMarketMaker.ts 的闪电贷示例展示了一个 PTB 中串联 borrow、swap、transfer、return 的方式。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • route 报价阶段和 PTB 构造阶段分离,报价快照写入 dry run 摘要。
  • 多跳 PTB 用前一跳输出 coin 连接后一跳,并设置最终最小输出。
  • 闪电贷组合必须保证 borrow 和 return 在同一 PTB 内闭合。

动手检查

  • 单池 swap 和多池 route 的错误面有什么差异?
  • dry run 后应检查哪些 balanceChanges?
  • 闪电贷 PTB 如果中途失败,为什么不会留下未归还状态?

ch12-12 Margin SDK 管理员接口和用户接口

返回本章

先定封装边界

SDK 小节先看封装边界:好的服务层不是隐藏 Move,而是减少对象、精度、权限和网络配置错误,同时保留 dry run 与错误定位能力。

源码入口

  • scripts/transactions/marginPrep.tssupplyToMarginPool.tsenableMarginVersion.ts:管理员/供应方脚本。
  • packages/deepbook_margin/sources/margin_manager.move:用户入口。
  • packages/deepbook_margin/sources/margin_pool.move:资金池 supply/withdraw。

SDK 读法

Margin 管理员路径来自 scripts/transactions/marginPrep.tsenableMarginVersion.tssupplyToMarginPool.ts

  • marginAdmin.enableVersion:启用 Margin 版本。
  • marginAdmin.newPythConfigaddConfigremoveConfig:维护价格源配置。
  • marginMaintainer.newProtocolConfigcreateMarginPool:创建资金池和利率参数。
  • marginPool.supplyToMarginPool:使用 supplier cap 向池子供给资产。

用户路径绑定 packages/deepbook_margin/sources/margin_manager.move,重点封装 newdepositwithdrawborrow_baseborrow_quoterepay_baserepay_quoteadd_conditional_order 和风险查询。每个用户交易都必须先检查 oracle 新鲜度、价格容忍度、风险率和可借额度。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • 管理员方法强制检查 admin/maintainer/supplier cap 和版本。
  • 用户 borrow/repay 前检查 oracle freshness、风险率、可借额度和 margin pool 状态。
  • Margin 服务和 Predict 服务共享配置风格,但对象和风险模型分开。

动手检查

  • Margin admin、maintainer、supplier 分别控制哪些操作?
  • 用户 borrow 前最关键的风险检查是什么?
  • 为什么不能把 Predict position 直接当作 Margin 风控输入?

ch12-13 Predict SDK 封装方式

返回本章

先定封装边界

读这一节时把自己当成 SDK 维护者:封装应该暴露业务意图,隐藏重复参数,同时保留 dry run 和错误定位能力。

源码入口

  • packages/predict/sources/predict.move:mint/redeem/supply/withdraw。
  • packages/predict/sources/predict_manager.move:manager deposit/withdraw/positions。
  • packages/predict/sources/registry.movemarket_key/range_key.move:创建对象和 market key。

SDK 读法

Predict 当前应按 Move 入口封装,不应假设已有与 Spot 完全同形的稳定 SDK。源码在 packages/predict/sources/predict.movepredict_manager.moveregistry.move。应用层可以先定义服务接口:

  • createPredictManager:绑定 registry::create_predict_manager 或当前发布版本的 manager 创建入口。
  • depositwithdraw:绑定 predict_manager::depositpredict_manager::withdraw
  • mintredeemsupply:绑定 predict.movemintredeemsupply
  • quote:优先使用只读查询或 dev inspect,不直接提交交易。

Predict 交易必须在配置中显式标注 package ID、registry ID、predict object ID、oracle ID、quote coin type、网络和版本状态。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • PredictService target 白名单覆盖 registry、predict_manager、predict、oracle。
  • 所有方法要求 package ID、registry ID、predict ID、oracle ID、quote coin type、version status。
  • quote 优先 dev inspect 或只读模拟,不提交真实交易。

动手检查

  • createPredictManagerdepositmint 分别绑定哪些 Move 模块?
  • Predict SDK 封装为什么必须暴露版本状态?
  • 没有 max cost 参数时 mint 服务如何做用户保护?

ch12-14 PTB、dry run、dev inspect 和 Gas

返回本章

先定封装边界

SDK 小节先看封装边界:好的服务层不是隐藏 Move,而是减少对象、精度、权限和网络配置错误,同时保留 dry run 与错误定位能力。

源码入口

  • scripts/utils/utils.tsdryRunTransactionBlock、gas payment、multisig bytes。
  • packages/deepbook/sources/pool.movepackages/deepbook_margin/sources/*packages/predict/sources/*:错误定位的 Move target。
  • book/ch12/code/s06-dry-run-helper/:错误解析工具骨架。

SDK 读法

PTB 是 SDK 服务层的基本返回值。后端构造后应:

  1. 设置 sender。
  2. 可选设置 gas payment。
  3. tx.build({ client })
  4. client.dryRunTransactionBlock({ transactionBlock })
  5. 检查 effects.status.status

scripts/utils/utils.ts 中的 prepareMultisigTx 会设置 gas price、expiration、gas payment,并在写入 multisig bytes 前执行 dry run。这是管理员交易的标准做法。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • 每个写交易执行 build、dry run、status 检查、错误解析,再交给签名流程。
  • 管理员 multisig bytes 固定 gas price、expiration 和 gas payment。
  • 错误码按 package/module/function 归类,覆盖 Spot、Margin、Predict。

动手检查

  • dry run 和 dev inspect 在 SDK 服务里分别解决什么问题?
  • Gas payment 固定对管理员 multisig 有什么价值?
  • Move abort 如何映射到具体入口和用户提示?

ch12-15 钱包签名、提交、确认和错误解析

返回本章

先定封装边界

读这一节时把自己当成 SDK 维护者:封装应该暴露业务意图,隐藏重复参数,同时保留 dry run 和错误定位能力。

源码入口

  • book/ch12/code/s05-wallet-flow/:钱包流程骨架。
  • scripts/utils/utils.ts:提交前 dry run 和交易 bytes。
  • packages/predict/sources/helper/constants.move:Predict abort/常量映射参考。

SDK 读法

前端钱包流程应只接收 Transaction 或 bytes:

const result = await wallet.signAndExecuteTransaction({
  transaction: tx,
  options: { showEffects: true, showObjectChanges: true, showBalanceChanges: true },
});

服务层不要要求用户上传私钥。确认阶段要读取 digest,再用 client.waitForTransaction 或轮询交易详情。失败时优先解析:

  • effects.status.error:Move abort、对象版本、余额、gas。
  • events:是否已经发出部分业务事件。
  • objectChanges:共享对象是否被写入。
  • balanceChanges:资产变化是否符合预期。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • 前端只接收 Transaction/bytes 并调用钱包签名提交。
  • 确认阶段读取 digest,再查询 effects、events、objectChanges、balanceChanges。
  • 错误解析优先展示可操作原因:余额、对象版本、权限、oracle stale、gas。

动手检查

  • 为什么后端不应要求用户上传私钥?
  • 交易失败但有 objectChanges 时应如何处理展示?
  • Predict mint 失败应重点解析哪些模块错误?

ch12-16 后端安全构造交易

返回本章

先定封装边界

SDK 小节先看封装边界:好的服务层不是隐藏 Move,而是减少对象、精度、权限和网络配置错误,同时保留 dry run 与错误定位能力。

源码入口

  • book/ch12/code/s05-wallet-flow/s06-dry-run-helper/:安全构造和错误解析。
  • scripts/config/constants.ts:对象 ID 白名单。
  • packages/predict/sources/predict.move:Predict 交易 target 白名单。

SDK 读法

后端服务可以构造交易,但不应替用户签名。推荐接口是:

POST /deepbook/orders/limit/build
{
  "sender": "0x...",
  "poolKey": "SUI_DBUSDC",
  "balanceManagerKey": "USER_MANAGER",
  "price": 1.23,
  "quantity": 10,
  "isBid": true
}

返回交易 bytes 和 dry run 摘要。后端只保存请求、报价快照和 dry run 结果,不保存私钥、不代签用户订单。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • 后端 API 校验 sender、pool/market key、manager key、金额、滑点和版本状态。
  • 响应包含 bytes、dry run 摘要、报价快照、过期时间和错误码。
  • 服务端日志保存 digest/requestId,不保存签名材料。

动手检查

  • 构造交易 API 哪些参数必须由后端二次校验?
  • 报价快照和 dry run 摘要为什么要一起返回?
  • Predict 交易缺少迁移状态时后端应如何拒绝?

ch12-17 SDK 单元测试、mock client 和 fixture

返回本章

先定封装边界

读这一节时把自己当成 SDK 维护者:封装应该暴露业务意图,隐藏重复参数,同时保留 dry run 和错误定位能力。

源码入口

  • book/ch12/code/s06-dry-run-helper/:dry run fixture 和错误解析。
  • scripts/config/constants.ts:配置测试样本。
  • packages/predict/sources/*:Predict fixture 应覆盖的对象类型。

SDK 读法

单元测试应 mock 三层:

  • 配置解析:网络、对象 ID、空值检查。
  • 交易构造:断言 PTB 包含预期 move target 或 SDK builder 调用。
  • dry run 结果:成功、Move abort、余额不足、对象不存在。

fixture 应包含典型 pool、BalanceManager、MarginManager、Predict object 的 object response。不要让单元测试依赖实时主网状态。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • mock SuiClient 覆盖 object query、dry run、waitForTransaction。
  • fixture 包含 pool、BalanceManager、MarginManager、Predict、Oracle、Vault。
  • 错误表覆盖 Move abort、对象不存在、版本冲突、余额不足、gas 不足。

动手检查

  • PTB 构造测试应断言 target 还是最终 digest?
  • 为什么单元测试不应依赖实时主网对象?
  • Predict fixture 中哪些版本状态必须覆盖?

ch12-18 本章实战:DeepBookService

返回本章

先定封装边界

SDK 小节先看封装边界:好的服务层不是隐藏 Move,而是减少对象、精度、权限和网络配置错误,同时保留 dry run 与错误定位能力。

源码入口

  • book/ch12/code/s02-deepbook-service/:Spot 服务骨架。
  • scripts/transactions/deepbookMarketMaker.tsprepBalanceManager.ts:SDK 调用参考。
  • packages/deepbook/sources/pool.movebalance_manager.move:Move 入口。

SDK 读法

book/ch12/code/s02-deepbook-service/ 给出 Spot 服务封装骨架:初始化 SDK、创建 BalanceManager、deposit、限价单、市价单、撤单、swap 和查询。它的核心原则是业务层只看到 poolKey 和金额,不接触 Move object 顺序。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • DeepBookService 对外只暴露 poolKey、managerKey、side、price、quantity 等业务参数。
  • 内部负责 SDK 初始化、对象解析、PTB 构造、dry run 和错误归类。
  • Spot 服务不处理 Predict oracle/vault 逻辑,只在共享配置层保持一致格式。

动手检查

  • DeepBookService 哪些方法返回 PTB,哪些方法返回查询结果?
  • 如何隐藏 Move object 顺序但保留错误可定位性?
  • BalanceManager 缺失时服务应自动创建还是返回待签 PTB?

ch12-19 本章实战:MarginService

返回本章

先定封装边界

读这一节时把自己当成 SDK 维护者:封装应该暴露业务意图,隐藏重复参数,同时保留 dry run 和错误定位能力。

源码入口

  • book/ch12/code/s03-margin-service/:Margin 服务骨架。
  • scripts/transactions/marginPrep.tssupplyToMarginPool.tsenableMarginVersion.ts:管理脚本。
  • packages/deepbook_margin/sources/margin_manager.movemargin_pool.move:用户与池子入口。

SDK 读法

book/ch12/code/s03-margin-service/ 给出 Margin admin 和用户接口骨架。管理员函数要求 cap 配置完整;用户函数要求对象状态、oracle freshness 和风险率检查。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

封装判断

  • MarginService admin 方法先校验 cap,用户方法先校验 manager、oracle 和风险率。
  • borrow/repay/supply/withdraw 分别记录 pool、asset、amount、risk summary。
  • 错误解析区分 cap 缺失、版本未启用、oracle stale、风险率不足。

动手检查

  • MarginService 与 DeepBookService 在风险检查上最大差异是什么?
  • 管理员 cap 配置为空时应该在哪一步失败?
  • 用户 borrow 后再下单的组合 PTB 需要新增哪些校验?

ch12-20 本章实战:PredictService

返回本章

先定封装边界

PredictService 的难点不是把 tx.moveCall 包成函数名,而是给一个仍在演进的协议面建立安全边界。服务必须明确 network、package ID、registry、predict object、oracle、quote asset、版本状态和 raw target 白名单;任何一项模糊,都可能让前端构造出能签名但不能解释的交易。

源码入口

  • book/ch12/code/s04-predict-service/:Predict 服务骨架。
  • packages/predict/sources/registry.movepredict_manager.movepredict.move:raw move call target。
  • packages/predict/simulations/src/runtime.ts:localnet PTB 执行和状态保存参考。

SDK 读法

book/ch12/code/s04-predict-service/ 给出 Predict 封装骨架。由于 Predict 处于独立包演进中,示例要求把 package/object IDs、网络和版本显式写入配置,并通过 raw tx.moveCall 绑定当前发布的 Move 入口。这里故意不把 Predict 包成和 Spot 完全同形的 SDK:Spot 的对象和公共 SDK 更稳定,Predict 的 Testnet 集成面仍需要更强的版本标注。

服务封装建议统一返回 Transaction 或交易 bytes、dry run 摘要和可读错误码。后端可以构造 PTB 和校验配置,但用户交易必须由钱包签名;管理员交易则按 scripts/utils/utils.ts 的 multisig/dry run 思路固定 gas、expiration 和 cap。

Predict 相关接口必须额外校验 predictVersionStatus、package ID、registry ID、predict object ID、oracle ID 和 quote coin type。由于迁移文档未把 Predict SDK、Indexer 或 Server 标为稳定完成,本章只把它们写成 raw Move 封装或未来服务边界。

一个合格的 PredictService 返回值应该能被日志系统和客服系统使用:输入对象、type arguments、range key、oracle freshness、dry run status、Move abort 和用户可读提示都要保留下来。否则用户只会看到“交易失败”,开发者也无法判断是 oracle stale、vault exposure、range key 还是 manager 余额问题。

封装判断

  • PredictService 初始化缺少 package/object IDs 或 predictVersionStatus 时直接失败。
  • raw tx.moveCall 封装 manager、deposit、mint、redeem、supply、withdraw,并维护 target 白名单。
  • dry run 错误映射覆盖 oracle stale、range key、ask bounds、vault exposure、limiter。
  • 用户交易只返回 PTB/bytes 给钱包签名;服务端不托管私钥。

动手检查

  • PredictService 为什么必须版本化,而不能只传 package ID?
  • mint 和 redeem 的 PTB 参数顺序分别需要哪些对象?
  • 如何向用户解释 oracle、vault 和 manager 三类失败?

ch06 DeepBookV3 Spot 交易开发

本章目标

本章从应用开发角度实现 DeepBookV3 Spot 交易:发现池子,展示 base/quote 和订单簿,创建或复用 BalanceManager,构造限价单、市价单、撤单 PTB,并处理成交事件、订单刷新、dry run、Gas 和常见错误。

本章学习阶梯

  • L1 先列出 Spot 应用最小功能:池子、订单簿、余额、下单、撤单。
  • L2 用 SDK/PTB 完成查询、充值、限价单、市价单和 dry run。
  • L3 把每个交易调用反向映射到 pool.move 入口。
  • L4 做出 CLI 或 Web 交易面板,并处理错误和刷新。

关键定义卡片

Spot 应用开发最先要读的是下单签名,而不是 SDK 封装名:

public fun place_limit_order<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    balance_manager: &mut BalanceManager,
    trade_proof: &TradeProof,
    client_order_id: u64,
    order_type: u8,
    self_matching_option: u8,
    price: u64,
    quantity: u64,
    is_bid: bool,
    pay_with_deep: bool,
    expire_timestamp: u64,
    clock: &Clock,
    ctx: &TxContext,
): OrderInfo

这就是应用侧的对象清单:PoolBalanceManagerTradeProofClock、base/quote 类型参数,以及整数化后的价格和数量。前端表单里的 decimal、side、order type、过期时间和费用选项,最后都要落到这个签名上。dry run 失败时,也应按这个签名反查:对象是否同网,manager 是否有余额,proof 是否有效,price/quantity 是否符合 tick 和 lot。

源码地图

  • packages/deepbook/sources/pool.move:Spot 交易对外入口。
  • packages/deepbook/sources/order_query.move:分页读取订单簿。
  • packages/deepbook/sources/book/order_info.move:订单生命周期、事件和错误码。
  • packages/deepbook/sources/book/order.move:订单字段、锁定余额和取消退款。
  • packages/deepbook/sources/book/book.move:撮合、插入、取消订单。
  • packages/deepbook/sources/state/state.move:订单创建、取消、成交后的账户状态更新。

小节目录

本章代码

  • book/ch06/code/s01-pool-list-cli/:列出池子和参数。
  • book/ch06/code/s02-place-limit-order/:限价买单/卖单 PTB。
  • book/ch06/code/s03-place-market-order/:市价单 PTB 和 dry run。
  • book/ch06/code/s04-cancel-order/:单笔与批量撤单。
  • book/ch06/code/s05-mini-terminal/:最小 CLI 交易终端设计。

Move 高阶穿插点

  • 写现货交易应用时,把每个 SDK 调用反向映射到 pool.move 的 public 函数。
  • 构造 PTB 前先列对象清单:Pool、BalanceManager、Clock、Registry、Coin、type arguments,一个都不要靠猜。
  • dry run 失败不是终点,要把错误归类到参数、权限、余额、版本、撮合策略或对象网络不一致。

常见错误

  • 把市价买入数量理解为 quote 数量;源码中仍是 base quantity。
  • 没有把 decimal 输入转换成 tick/lot 对齐的整数。
  • 市价单未设置滑点保护或未 dry run。
  • 期待所有成交订单都有 OrderPlaced;完全吃单不会入簿。
  • 撤单重试使用严格批量入口,导致一个无效订单使整批失败。

本章检查清单

  • 能写出限价单和市价单的 PTB 参数。
  • 能按 OrderFilled 字段重建个人成交。
  • 能使用 order_query::iter_orders 分页读取订单簿。
  • 能区分 cancel_ordercancel_live_order
  • 能把 Move abort code 转成前端提示。

进阶练习

  1. 实现一个本地订单簿聚合器,把 iter_orders 返回的订单按价格聚合。
  2. 写一个市价单 dry run 工具,输出预估成交均价、手续费和最差可接受输出。
  3. 给做市机器人加库存上限,超过上限自动停止单侧报价。

ch06-01 Spot 应用的最小功能集

返回本章

先从用户动作开始

这一节把“Spot 应用的最小功能集”当作应用流程来读:先看用户要完成什么,再反推 PTB 需要哪些对象、参数、dry run 和失败处理。

源码入口

从应用反推链上

最小 Spot 应用需要六个能力:池子发现、余额管理、下单、撤单、订单簿展示、成交和订单状态刷新。DeepBook 的交易入口都在 pool.moveplace_limit_orderplace_market_ordercancel_ordercancel_orderscancel_live_ordercancel_live_orderscancel_all_orders

应用状态应按三类缓存组织:

  • 市场状态:pool ID、base/quote type、tick size、lot size、min size、当前 fee 参数。
  • 用户状态:wallet coins、BalanceManager ID、manager 内 base/quote/DEEP 余额、open orders。
  • 交易状态:最近 dry run、待签名 PTB、提交 digest、事件确认结果。

交易按钮不可只依赖 UI 表单校验。提交前必须 dry run PTB,并把 Move abort 翻译成用户能处理的提示。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

精度和池子发现是所有交易动作的前置条件。用户输入的十进制价格和数量必须转换为 tick/lot 对齐的整数,再交给 pool.move,否则错误会在链上校验阶段暴露。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • Spot 应用的最小功能集 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-02 交易对发现、池子选择和显示

返回本章

先从用户动作开始

先从用户动作开始。“交易对发现、池子选择和显示”在产品里会表现为按钮、表单、状态刷新或错误提示;链上仍然会落到固定的 Pool 和 BalanceManager 入口。

源码入口

从应用反推链上

池子由泛型类型 Pool<BaseAsset, QuoteAsset> 表示。PoolCreated 事件记录 pool_idtick_sizelot_sizemin_sizetaker_feemaker_feewhitelisted_pool。如果使用 registry,需要通过 registry.get_pool_id<BaseAsset, QuoteAsset> 获取已注册池子 ID。

前端显示时,base 是下单数量单位,quote 是价格单位。place_limit_orderquantity 始终以 base asset 计;买单支付 quote,卖单支付 base。价格、数量都使用链上整数精度,不能直接用浮点数提交。

开发注意事项:

  • 池子类型必须与用户选择的 base/quote 完全一致,反向交易对不是同一个泛型类型。
  • stable pool、whitelisted pool 会影响默认费率。
  • 提交前检查 registered_pool() 和版本状态,避免 stale pool。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

精度和池子发现是所有交易动作的前置条件。用户输入的十进制价格和数量必须转换为 tick/lot 对齐的整数,再交给 pool.move,否则错误会在链上校验阶段暴露。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 交易对发现、池子选择和显示 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-03 精度、tick size、lot size 和最小量

返回本章

先从用户动作开始

这一节把“精度、tick size、lot size 和最小量”当作应用流程来读:先看用户要完成什么,再反推 PTB 需要哪些对象、参数、dry run 和失败处理。

源码入口

从应用反推链上

create_permissionless_pool 要求 tick_sizelot_sizemin_size 合法。下单时 order_info::new 会校验价格、数量和过期时间,相关错误包括 EOrderInvalidPrice = 0EOrderBelowMinimumSize = 1EOrderInvalidLotSize = 2EInvalidExpireTimestamp = 3

UI 输入必须先转换为整数:

  • price 必须落在 tick size 网格上。
  • quantity 必须是 lot size 的整数倍。
  • quantity 必须大于等于 min size。
  • 市价单仍然以 base quantity 表示,只是 price 由 pool.place_market_order 自动设为买单 MAX_PRICE 或卖单 MIN_PRICE

应用侧应在输入框旁展示最小步进,而不是等链上 abort。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

精度和池子发现是所有交易动作的前置条件。用户输入的十进制价格和数量必须转换为 tick/lot 对齐的整数,再交给 pool.move,否则错误会在链上校验阶段暴露。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 精度、tick size、lot size 和最小量 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-04 钱包连接和交易签名

返回本章

先从用户动作开始

先从用户动作开始。“钱包连接和交易签名”在产品里会表现为按钮、表单、状态刷新或错误提示;链上仍然会落到固定的 Pool 和 BalanceManager 入口。

源码入口

从应用反推链上

DeepBook 交易通常是 PTB。钱包只签名交易,资产扣划发生在 Move 调用里。典型 PTB 顺序是:

  1. 选择 BalanceManager shared/owned object。
  2. 创建 TradeProof:owner 用 generate_proof_as_owner,机器人用 generate_proof_as_trader
  3. 调用 pool.place_limit_orderpool.place_market_order
  4. 可选调用 pool.withdraw_settled_amounts 把历史 settled 余额落回 manager。

不要把 proof 当作长期凭证保存。它是交易内临时值,适合在 PTB 中构造后立即传入 pool。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

PTB 构造要把 type arguments、pool 对象、manager/proof、价格、数量、过期时间和 self-match 策略放在同一个检查清单中。签名前 dry run 一次,既能估算 Gas,也能把 Move abort 提前翻译成用户能理解的错误。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 钱包连接和交易签名 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-05 创建 BalanceManager 和首次充值

返回本章

先从用户动作开始

这一节把“创建 BalanceManager 和首次充值”当作应用流程来读:先看用户要完成什么,再反推 PTB 需要哪些对象、参数、dry run 和失败处理。

源码入口

从应用反推链上

首次交易流程:

  1. balance_manager::new 创建 manager。
  2. 可选 register_balance_manager,方便账户查询。
  3. deposit<BaseAsset>deposit<QuoteAsset>deposit<DEEP> 充值。
  4. 生成 proof 并下单。

买单需要 quote 余额和手续费余额;卖单需要 base 余额和手续费余额。当前源码注释说明 place_limit_order 当前版本要求 pay_with_deep 为 true,应用应优先走 DEEP 手续费路径,并在 dry run 中确认 DEEP 足够。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

精度和池子发现是所有交易动作的前置条件。用户输入的十进制价格和数量必须转换为 tick/lot 对齐的整数,再交给 pool.move,否则错误会在链上校验阶段暴露。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 创建 BalanceManager 和首次充值 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-06 限价买单和限价卖单 PTB

返回本章

先从用户动作开始

先从用户动作开始。“限价买单和限价卖单 PTB”在产品里会表现为按钮、表单、状态刷新或错误提示;链上仍然会落到固定的 Pool 和 BalanceManager 入口。

源码入口

从应用反推链上

pool.place_limit_order 参数含义:

  • client_order_id:客户端自定义 ID,用于事件和本地订单映射。
  • order_type:no restriction、IOC、FOK、post-only 等常量。
  • self_matching_option:自成交处理策略。
  • price:限价,买单最高愿付,卖单最低愿收。
  • quantity:base 数量。
  • is_bid:true 为买单,false 为卖单。
  • pay_with_deep:是否用 DEEP 支付手续费。
  • expire_timestamp:订单过期毫秒时间戳。

资金路径:place_order_int 创建 OrderInfobook.create_order 撮合,state.process_create 计算 owed/settled,vault.settle_balance_manager 从 manager 扣 quote/base/DEEP 或把成交所得转回 manager,随后发出订单事件。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

PTB 构造要把 type arguments、pool 对象、manager/proof、价格、数量、过期时间和 self-match 策略放在同一个检查清单中。签名前 dry run 一次,既能估算 Gas,也能把 Move abort 提前翻译成用户能理解的错误。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 限价买单和限价卖单 PTB 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-07 市价买入和市价卖出 PTB

返回本章

先从用户动作开始

这一节把“市价买入和市价卖出 PTB”当作应用流程来读:先看用户要完成什么,再反推 PTB 需要哪些对象、参数、dry run 和失败处理。

源码入口

从应用反推链上

pool.place_market_order 也是以 base quantity 下单。买入时 is_bid = true,价格自动是 constants::max_price();卖出时 is_bid = false,价格自动是 constants::min_price()。函数内部把 order type 设为 immediate_or_cancel,未成交数量会取消。

市价单前必须调用 dry run:

  • get_quote_quantity_in(target_base_quantity, pay_with_deep, clock) 估算买入目标 base 需要多少 quote。
  • get_base_quantity_in(target_quote_quantity, pay_with_deep, clock) 估算卖出得到目标 quote 需要多少 base。
  • get_quantity_outget_quantity_out_input_fee 估算给定输入能拿到多少输出。

设置 min_quote_out 或前端滑点保护时,不要只看最优价,要按订单簿深度逐档计算。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

PTB 构造要把 type arguments、pool 对象、manager/proof、价格、数量、过期时间和 self-match 策略放在同一个检查清单中。签名前 dry run 一次,既能估算 Gas,也能把 Move abort 提前翻译成用户能理解的错误。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 市价买入和市价卖出 PTB 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-08 订单状态刷新和深度展示

返回本章

先从用户动作开始

先从用户动作开始。“订单状态刷新和深度展示”在产品里会表现为按钮、表单、状态刷新或错误提示;链上仍然会落到固定的 Pool 和 BalanceManager 入口。

源码入口

从应用反推链上

order_query::iter_orders 是链上订单簿分页接口。参数包括 start_order_idend_order_idmin_expire_timestamplimitbids。买单从高到低遍历,卖单从低到高遍历。返回 OrderPage { orders, has_next_page }

前端深度展示应把同价位订单聚合为 price level。分页时保留最后一个 order ID 作为下一页 start,且过滤已过期订单。min_expire_timestamp 可以设为当前时间,减少前端展示过期订单。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

读模型要区分链上查询和 indexer 缓存。packages/deepbook/sources/order_query.move 适合读取当前订单簿快照,个人成交历史则应以事件和交易 digest 为准;前端本地缓存只能做临时加速,不能覆盖链上最终状态。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 订单状态刷新和深度展示 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-09 成交历史、个人成交和未成交订单

返回本章

先从用户动作开始

这一节把“成交历史、个人成交和未成交订单”当作应用流程来读:先看用户要完成什么,再反推 PTB 需要哪些对象、参数、dry run 和失败处理。

源码入口

从应用反推链上

OrderInfo 会发出三类核心事件:

  • OrderPlaced:订单进入订单簿。
  • OrderFilled:maker 订单被 taker 成交,包含 maker/taker order ID、client order ID、价格、数量、手续费和双方 manager ID。
  • OrderFullyFilled:订单完全成交。

个人成交可按 maker_balance_manager_idtaker_balance_manager_id 过滤 OrderFilled。未成交订单可以来自两处:链上 order_query 的 orderbook 加本地用户 manager ID 过滤,或 indexer 维护的 open orders 表。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

读模型要区分链上查询和 indexer 缓存。packages/deepbook/sources/order_query.move 适合读取当前订单簿快照,个人成交历史则应以事件和交易 digest 为准;前端本地缓存只能做临时加速,不能覆盖链上最终状态。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 成交历史、个人成交和未成交订单 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-10 撤单、批量撤单和失败重试

返回本章

先从用户动作开始

先从用户动作开始。“撤单、批量撤单和失败重试”在产品里会表现为按钮、表单、状态刷新或错误提示;链上仍然会落到固定的 Pool 和 BalanceManager 入口。

源码入口

从应用反推链上

pool.cancel_order 要求订单属于当前 balance_manager,否则 state.process_cancel 会失败。取消后 book.cancel_order 移除订单,process_cancel 计算退款,vault.settle_balance_manager 把锁定资产退回 manager,订单发出 canceled 事件。

批量撤单有三类入口:

  • cancel_orders:任一订单失败则整笔交易失败。
  • cancel_live_order:如果订单不存在、已成交、已取消或不属于该 manager,则直接跳过。
  • cancel_live_orders:批量跳过无效订单,适合前端重试和机器人清理。

生产应用对用户手动撤单可用严格 cancel_order,对周期性清理应使用 live 版本。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

精度和池子发现是所有交易动作的前置条件。用户输入的十进制价格和数量必须转换为 tick/lot 对齐的整数,再交给 pool.move,否则错误会在链上校验阶段暴露。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 撤单、批量撤单和失败重试 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-11 交易前模拟、Gas 和错误提示

返回本章

先从用户动作开始

这一节把“交易前模拟、Gas 和错误提示”当作应用流程来读:先看用户要完成什么,再反推 PTB 需要哪些对象、参数、dry run 和失败处理。

源码入口

从应用反推链上

每笔下单 PTB 前至少检查:

  • base/quote/DEEP manager 余额,含 settled balances。
  • price、quantity 是否满足 tick、lot、min size。
  • expire_timestamp 是否晚于当前时间。
  • pool 是否 registered,版本是否启用。
  • DEEP price 是否存在,或输入资产 fee 是否有额外余额。

常见订单错误码来自 order_info.move:post-only 穿越订单簿触发 EPOSTOrderCrossesOrderbook = 5;FOK 无法完全成交触发 EFOKOrderCannotBeFullyFilled = 6;市价单不能 post-only,触发 EMarketOrderCannotBePostOnly = 7;自成交策略取消 taker 时可能触发 ESelfMatchingCancelTaker = 8

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

精度和池子发现是所有交易动作的前置条件。用户输入的十进制价格和数量必须转换为 tick/lot 对齐的整数,再交给 pool.move,否则错误会在链上校验阶段暴露。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 交易前模拟、Gas 和错误提示 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-12 做市机器人数据和交易循环

返回本章

先从用户动作开始

先从用户动作开始。“做市机器人数据和交易循环”在产品里会表现为按钮、表单、状态刷新或错误提示;链上仍然会落到固定的 Pool 和 BalanceManager 入口。

源码入口

从应用反推链上

做市机器人最小循环:

  1. 拉取池子参数和订单簿。
  2. 拉取自己的 open orders 和 manager 余额。
  3. 根据目标价差生成报价。
  4. 撤掉偏离报价。
  5. 提交新的限价单。
  6. 处理 OrderFilledOrderFullyFilled,更新库存。

机器人应持有 TradeCap,不持有 WithdrawCap。每轮先 dry run,失败时按 abort code 分类:余额不足暂停报价,版本错误刷新配置,订单已消失则忽略撤单失败。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

自动化交易循环应把“取行情 -> 计算目标报价 -> dry run -> 提交 -> 监听事件 -> 刷新库存”拆成独立步骤。任何一步出现余额不足、订单已消失或版本暂停,都应该停止当前轮报价而不是继续重试。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 做市机器人数据和交易循环 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-13 最小命令行交易终端

返回本章

先从用户动作开始

这一节把“最小命令行交易终端”当作应用流程来读:先看用户要完成什么,再反推 PTB 需要哪些对象、参数、dry run 和失败处理。

源码入口

从应用反推链上

CLI 交易终端应提供 marketsbalancesbooklimitmarketcancelfills 命令。所有命令都接受 --pool 和 base/quote type 参数,避免把不同泛型池混在一起。

命令行输出应使用整数和人类可读两种格式。提交交易时打印 digest、order ID、client order ID 和事件摘要,方便和 indexer 对账。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

自动化交易循环应把“取行情 -> 计算目标报价 -> dry run -> 提交 -> 监听事件 -> 刷新库存”拆成独立步骤。任何一步出现余额不足、订单已消失或版本暂停,都应该停止当前轮报价而不是继续重试。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 最小命令行交易终端 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch06-14 最小 Web 交易面板

返回本章

先从用户动作开始

先从用户动作开始。“最小 Web 交易面板”在产品里会表现为按钮、表单、状态刷新或错误提示;链上仍然会落到固定的 Pool 和 BalanceManager 入口。

源码入口

从应用反推链上

Web 面板第一屏应是可交易界面,而不是介绍页。布局建议:左侧市场列表,中间订单簿和价格图,右侧下单面板,底部个人订单和成交。按钮状态直接绑定 dry run 结果。

前端要避免乐观假设:交易提交成功不等于订单已入簿,市价单可能完全成交不产生 OrderPlaced,IOC 未成交部分会取消。最终状态以事件和订单查询为准。

工程旁白:Spot 应用先看用户状态机:市场、余额、订单、提交、确认。链上提交仍然落到 pool.movebalance_manager.move;UI 只是把这些固定入口组织成用户能理解的交易动作。

自动化交易循环应把“取行情 -> 计算目标报价 -> dry run -> 提交 -> 监听事件 -> 刷新库存”拆成独立步骤。任何一步出现余额不足、订单已消失或版本暂停,都应该停止当前轮报价而不是继续重试。

写应用时

  • 所有用户输入先做 decimal、tick size、lot size 和 min size 转换,再构造 PTB。
  • 提交前 dry run,提交后用 digest、事件和 order_query.move 刷新状态。
  • UI/CLI 中分开展示 wallet coins、manager 余额、open orders 和最近一次交易状态。

动手检查

  • 最小 Web 交易面板 需要哪些对象 ID、type arguments、cap/proof 和数值参数?
  • dry run 失败时应给用户显示哪类提示:余额、精度、订单状态、版本暂停还是 Gas?
  • 交易成功后应用应该依赖事件、order query 还是本地缓存更新页面?

ch03 DeepBookV3 总体架构

本章目标

  • 建立 DeepBookV3 的全局对象模型:PoolBookStateVaultBalanceManagerRegistry 分别承担什么职责。
  • 能从一次下单交易反推源码调用链、资金结算链和事件链。
  • 理解哪些状态集中在共享 Pool 上,哪些状态被抽象到 BalanceManager,以及这种拆分如何影响并发交易设计。
  • 能解释 DeepBookV3 与 DeepBook Margin、DeepBook Predict 的依赖边界,而不是把不同协议的借贷和预测市场状态混在一起。

本章学习阶梯

  • L2 先从一次下单的对象清单理解架构。
  • L3 追 Pool -> Book -> State -> Vault -> BalanceManager 的主调用链。
  • L4 解释版本、注册、capability 和对象借用如何影响真实交易。
  • L5 能画出协议升级或交易失败时的排查路径。

关键定义卡片

先看 Pool,不要急着进入撮合函数:

public struct Pool<phantom BaseAsset, phantom QuoteAsset> has key {
    id: UID,
    inner: Versioned,
}

public struct PoolInner<phantom BaseAsset, phantom QuoteAsset> has store {
    allowed_versions: VecSet<u64>,
    pool_id: ID,
    book: Book,
    state: State,
    vault: Vault<BaseAsset, QuoteAsset>,
    deep_price: DeepPrice,
    registered_pool: bool,
}

这段定义说明 DeepBookV3 的池不是“只有订单簿”。Pool 是外部 PTB 传入的共享对象,PoolInner 才是业务状态。book 管撮合,state 管账户、历史、费用和治理,vault 管真实资产,allowed_versions 管升级后的可用版本。读 place_limit_order 时要把这四个内部对象都带上,否则会误以为下单只是 Book 内部操作。

源码地图

小节目录

本章代码

  • code/s01-architecture-map/:从源码提取模块依赖关系,生成架构图输入。
  • code/s02-pool-object-query/:查询链上 Pool 对象字段,核对 PoolInner 中的 book/state/vault 结构。
  • code/s03-event-flow/:按交易 digest 解析 DeepBook 事件,复原一次下单的事件链。

Move 高阶穿插点

  • 读架构时先画对象借用图:哪些是 &mut shared object,哪些只是只读参数,哪些资源会被消费。
  • VersionedRegistry 和 capability 不是外围细节,它们决定生产环境升级、暂停和权限边界。
  • 把每个模块都绑定到一种 Move 状态迁移:创建、借用、写入、拆分、合并、销毁或事件发出。

常见错误

  • 错误:把 Pool 当成只包含订单簿的对象。实际 Pool 还包含 State、Vault、DeepPrice 和版本控制。
  • 错误:下单前只检查钱包 coin。实际订单资金来自 BalanceManager,必须检查 BalanceManager 余额和 TradeProof 权限。
  • 错误:认为 Book 负责费用和余额。Book 只负责撮合和挂单;费用在 State/OrderInfo 计算,资产在 Vault 结算。
  • 错误:把 DeepBookV3 闪电贷与 Margin 借贷混写。V3 闪电贷入口在 pool.move,底层是 vault.move 的 hot potato。
  • 错误:忽略版本开关。Pool::load_innerRegistry::load_inner 都可能因 package version 禁用而 abort。

本章检查清单

  • 能画出 Pool、Book、State、Vault、BalanceManager、Registry 的关系。
  • 能说出 place_limit_ordervault::settle_balance_manager 的完整调用链。
  • 能解释 Pool<BaseAsset, QuoteAsset> 中 base/quote 泛型的意义。
  • 能说明为什么 BalanceManager 是交易账户而不是钱包余额。
  • 能区分 DeepBookV3 闪电贷和 DeepBook Margin 借贷。

进阶练习

  • 练习 1:打开 pool.move,列出每个 public mutative 函数需要 mutable borrow 的对象,并判断它们的并发边界。
  • 练习 2:从 Registry::register_pool 出发,解释为什么反向交易对不能重复注册。
  • 练习 3:用一次部分成交订单,画出 OrderInfoFillStateVault 的状态变化表。

ch03-01 全景图:Pool、Book、State、Vault、BalanceManager、Registry

返回本章

先抓住结构

读“全景图:Pool、Book、State、Vault、BalanceManager、Registry”时先画边界。一个真实协议最容易读乱的地方,不是函数太多,而是不知道 Pool、Book、State、Vault 和 BalanceManager 各自负责哪一段。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

DeepBookV3 的核心不是一个单独的“订单簿对象”,而是以 Pool<BaseAsset, QuoteAsset> 为共享对象入口,把撮合、账户状态和资产保管组合到同一个池内版本化状态中。

flowchart LR
    User["Trader / App"] --> BM["BalanceManager<br/>用户交易账户"]
    User --> Pool["Pool<Base, Quote><br/>共享交易入口"]
    Registry["Registry<br/>池注册 / 版本 / 权限"] --> Pool
    Pool --> Book["Book<br/>bids / asks / order id"]
    Pool --> State["State<br/>accounts / history / governance"]
    Pool --> Vault["Vault<Base, Quote><br/>base / quote / DEEP"]
    Book --> Order["Order"]
    Book --> OI["OrderInfo"]
    OI --> Fill["Fill"]
    State --> Account["Account"]
    Vault <--> BM

pool.movePool 只有 id: UIDinner: Versioned。真正业务状态在 PoolInner<BaseAsset, QuoteAsset>allowed_versionspool_idbookstatevaultdeep_priceregistered_pool。这意味着协议可以用 Versioned 控制包版本可用性,同时保留共享对象 ID 作为外部集成的稳定入口。

源码旁白:看到 Versioned 时,不要把它当作普通包装字段略过。生产协议升级时,外部应用依赖稳定对象 ID,而内部状态需要版本门控;这正是 Pool 外壳很薄、PoolInner 承载业务状态的原因。

开发实践上,所有交易 PTB 都应把 Pool 当作唯一撮合入口,把 BalanceManager 当作用户资金账户。不要直接假设钱包 coin 就是交易余额;下单时 Pool 只和 BalanceManager 结算差额。

阅读补充

全景图的关键不是把模块名背下来,而是给每个模块放一个动词:Pool 接入口,Book 撮合,State 记账户/费用/历史,Vault 保管并结算资产,BalanceManager 表示用户交易账户,Registry 管版本和注册。

读源码时按一次下单横穿这些模块,而不是逐文件孤立阅读。只要能解释一个订单什么时候进入 Book、什么时候影响 State、什么时候由 Vault 改余额,就已经掌握了架构主线。

工程判断

  • 架构图上同时标注对象职责和调用方向。
  • 不要把 Book 画成资产保管方,资产最终在 Vault 和 BalanceManager 间结算。
  • Registry 不是交易撮合模块,但版本和注册错误会阻断交易。

读完以后问自己

  • 六个核心模块各自的一个动词职责是什么?
  • 一次下单为什么会同时影响 Book、State 和 Vault?
  • BalanceManager 为什么应画在用户侧和 Pool 之间?

ch03-02 package、shared object 与 capability 布局

返回本章

先抓住结构

先把“package、shared object 与 capability 布局”放进 DeepBook 的对象图里。这里不是罗列模块,而是建立阅读顺序:入口在哪里,状态放在哪里,资金最终在哪里结算。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

registry.moveinit 创建并共享 Registry,同时把 DeepbookAdminCap 转给发布者。pool.movecreate_pool 创建 Pool<BaseAsset, QuoteAsset> 后调用 transfer::share_object(pool),所以每个交易对是共享对象。balance_manager.moveBalanceManager 也是 has key, store 的对象,可作为共享交易账户使用。

权限分成三层:

  • 协议管理权限:DeepbookAdminCap 用于版本、白名单、注册表管理和 admin pool 创建。
  • 应用授权:Registry::authorize_app<App> 写入 AppKey<App> 动态字段,允许应用访问受保护能力,例如 new_with_custom_owner_caps
  • 用户交易授权:TradeCapDepositCapWithdrawCap 绑定一个 BalanceManagerTradeProof 是可丢弃的临时证明,传入 Pool 后由 VaultBalanceManager 校验。

交易前检查应包括:Pool 对象版本是否启用、BalanceManager 是否归属或有对应 cap、交易资产类型是否与池泛型一致、订单参数是否满足 tick/lot/min size。

阅读补充

DeepBook 的布局可以分三层看:package 提供不可变代码入口,shared object 承载 Pool/Registry 这类全局可访问状态,capability/proof 承载管理员、应用或用户交易授权。交易成功必须同时满足代码入口正确、共享对象版本可用、权限对象有效。

权限阅读建议从 struct 定义开始,再找创建函数、转移函数和校验函数。只有看到 cap 如何产生和在哪里被验证,才能判断它是否只是展示字段,还是实际保护了状态修改。

工程判断

  • 交易构造文档要分开列 package ID、shared object ID 和 cap/proof。
  • cap 的创建、转移、销毁和验证函数要成组阅读。
  • 版本 abort 不属于余额问题,应在权限和对象检查前单独确认。

读完以后问自己

  • package、shared object、capability 分别解决什么问题?
  • TradeProof 在调用链中在哪里被验证?
  • 为什么 package ID 正确仍可能因为 shared object 版本失败?

ch03-03 Pool 的泛型资产设计

返回本章

先抓住结构

读“Pool 的泛型资产设计”时先画边界。一个真实协议最容易读乱的地方,不是函数太多,而是不知道 Pool、Book、State、Vault 和 BalanceManager 各自负责哪一段。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

关键定义

先看 Pool 的定义:

public struct Pool<phantom BaseAsset, phantom QuoteAsset> has key {
    id: UID,
    inner: Versioned,
}

BaseAssetQuoteAsset 是 phantom 类型参数。它们不作为运行时字段保存,但会进入类型系统,决定这个池到底是 SUI/USDCDEEP/SUI,还是另一个资产对。也就是说,市场身份不是一个字符串字段,而是 Move 类型的一部分。

池创建入口也把这个设计暴露出来:

public fun create_permissionless_pool<BaseAsset, QuoteAsset>(
    registry: &mut Registry,
    tick_size: u64,
    lot_size: u64,
    min_size: u64,
    creation_fee: Coin<DEEP>,
    ctx: &mut TxContext,
): ID

这段签名告诉开发者两件事:第一,创建池必须给出 base/quote 类型参数;第二,tick、lot、min size 是链上规则,不是前端展示规则。SDK 如果只让用户输入 "SUI/USDC",最后仍然必须把它翻译成 BaseAssetQuoteAsset 两个完整 Move type。

读架构

Pool<phantom BaseAsset, phantom QuoteAsset> 把交易对编码到类型系统里。BaseAsset 表示成交数量的单位,QuoteAsset 表示计价资产。place_limit_orderplace_market_orderquantity 都是 base 数量;买单用 quote 支付,卖单用 base 支付。

create_pool 明确校验:

  • BaseAssetQuoteAsset 不能是同一种类型。
  • tick_size 必须是 10 的幂。
  • lot_size >= 1000 且是 10 的幂。
  • min_size > 0、是 10 的幂,并且能被 lot_size 整除。
  • 池不能同时是 whitelisted pool 和 stable pool。

这些约束不是 UI 层规则,而是链上 abort 条件。前端或 bot 在构造交易前应先读取池参数并在本地做同样校验,避免浪费 gas。

阅读补充

Pool 的泛型资产设计把市场身份放进类型系统:Pool<BaseAsset, QuoteAsset> 明确 base 和 quote 的方向,反向交易对必须作为另一个类型组合处理。价格、数量和最小下单单位再由 tick/lot/min size 约束,防止订单簿出现无法结算或无法排序的粒度。

阅读创建池逻辑时,把类型约束和数值约束分开列。类型约束回答“这是哪个市场”,数值约束回答“这个市场允许怎样的价格和数量”。

工程判断

  • 前端和 SDK 必须固定 base/quote 顺序,展示反向价格时不要反向调用错误池。
  • 创建池参数要验证 tick size、lot size、min size 的幂和整除关系。
  • 对象查询时同时核对 Pool 类型和注册表记录。

读完以后问自己

  • 为什么 BaseAsset 与 QuoteAsset 不能相同?
  • tick size 和 lot size 分别约束什么?
  • 反向交易对为什么不能简单复用同一个 Pool 类型?

ch03-04 Pool 是入口,Book 是撮合核心

返回本章

先抓住结构

先把“Pool 是入口,Book 是撮合核心”放进 DeepBook 的对象图里。这里不是罗列模块,而是建立阅读顺序:入口在哪里,状态放在哪里,资金最终在哪里结算。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

Pool 对外暴露 place_limit_orderplace_market_orderswap_exact_*modify_ordercancel_orderwithdraw_settled_amounts 等接口。Book 不直接暴露给外部调用者;它是 PoolInner 的内部字段。

一次限价单的核心调用链是:

pool::place_limit_order
  -> pool::place_order_int
    -> order_info::new
    -> book::create_order
      -> order_info::validate_inputs
      -> utils::encode_order_id
      -> book::match_against_book
        -> order_info::match_maker
          -> order::generate_fill
      -> book::inject_limit_order
    -> state::process_create
    -> vault::settle_balance_manager
    -> order_info events

Book 只决定“能不能成交、成交多少、是否插入订单簿”。账户开放订单、历史费率、maker/taker 费用和资金扣划都不在 Book 中完成,而是在 StateVault 中完成。

阅读补充

Pool 负责把外部交易参数转换成协议内部可执行流程:加载版本、校验参数、构造 OrderInfo、调用 Book,最后协调 State 和 Vault。Book 不应该承担权限、费用和资产保管,它的核心职责是维护 bids/asks、匹配价格和生成 fill。

阅读调用链时,从 pool.move 的 public entry/public 函数进入,再跳到 book.move 看撮合循环,最后回到 order_info.movefill.move 看成交如何记录。这个来回跳转能避免误以为某个单文件包含完整交易语义。

工程判断

  • 入口函数说明外部 API,Book 函数说明撮合规则,两者不要混写。
  • 调试撮合异常时记录 order id、side、price、quantity 和 order type。
  • 费用和余额问题要继续追到 State/Vault,而不是停在 Book。

读完以后问自己

  • Pool 在下单流程中承担哪些 Book 以外的职责?
  • Book 生成 fill 后,为什么还需要 OrderInfo?
  • 撮合成功但余额异常时应该继续读哪个模块?

ch03-05 State:账户、历史、治理、费用、参数

返回本章

先抓住结构

读“State:账户、历史、治理、费用、参数”时先画边界。一个真实协议最容易读乱的地方,不是函数太多,而是不知道 Pool、Book、State、Vault 和 BalanceManager 各自负责哪一段。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

state.moveState 包含 accounts: Table<ID, Account>history: Historygovernance: GovernanceState::process_create 是撮合之后的账务中枢:

  • 更新治理和历史:governance.update(ctx)history.update(...)
  • 处理每个 maker fill:process_fills 会更新 maker 账户、成交量和应结算余额。
  • 更新 taker 账户:确保账户存在,读取交易量和 stake。
  • 根据 trade_params、账户 stake、历史交易量和 EWMA 惩罚计算 taker fee。
  • 如果新订单剩余数量被插入订单簿,则加入 account open orders。
  • 调用 order_info.calculate_partial_fill_balances 得到本次 taker 的 settledowed

这说明订单状态和资金状态是分阶段写入的:Book 先改变撮合状态,State 再把成交解释为账户余额变化,Vault 最后才移动真实资产。

阅读补充

State 是撮合之后的会计和参数层。Book 告诉协议成交了什么,State 决定这些成交如何影响 maker/taker 账户、交易量、stake、费率、rebate 和历史统计。

阅读 State 时建议按函数职责拆表:账户 open orders、已结算余额、历史 epoch 数据、治理参数、费率计算。把这些混成一个“状态更新”会让费用来源和最终 owed/settled 难以解释。

工程判断

  • 费用问题优先查 trade params、stake、历史交易量和 governance 配置。
  • 账户 open orders 与 Vault 资产余额不是同一类状态。
  • State 输出的 owed/settled 需要继续交给 Vault 结算。

读完以后问自己

  • State 为什么不负责订单簿排序?
  • maker/taker fee 计算依赖哪些状态?
  • owedsettled 为什么还不是最终钱包余额?

ch03-06 Vault:最终资产保管和结算

返回本章

先抓住结构

先把“Vault:最终资产保管和结算”放进 DeepBook 的对象图里。这里不是罗列模块,而是建立阅读顺序:入口在哪里,状态放在哪里,资金最终在哪里结算。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

vault.moveVault<BaseAsset, QuoteAsset> 保存三类余额:base_balancequote_balancedeep_balancesettle_balance_manager 接收 balances_outbalances_in

  • balances_out.base() > balances_in.base(),Vault 把差额 split 出来并存入 BalanceManager。
  • balances_in.base() > balances_out.base(),BalanceManager 通过 withdraw_with_proof 提供差额,Vault join 到池余额。
  • quote 和 DEEP 使用同样模式。

因此结算是“净额结算”,不是每个 fill 都立即转 coin。开发者排查资金问题时,应同时看 OrderInfoFillState 返回的 Balances,以及最终 BalanceManager 的余额变化。

阅读补充

Vault 是资金守恒的最后关口。State 计算出本次交易的输入输出差额后,Vault 根据 base、quote、DEEP 三类余额方向,从池内余额 split 给 BalanceManager,或从 BalanceManager withdraw 后 join 回池内余额。

阅读 Vault 时要把每个方向都写成“谁多了、谁少了、差额是多少”。这样能快速识别 taker 支付 quote 获得 base、maker 收入、DEEP fee 这几条资金线是否被正确结算。

工程判断

  • 结算表按 base、quote、DEEP 三列记录差额。
  • Vault 调用 BalanceManager 时必须验证 proof,不要绕过授权解释资金变化。
  • 闪电贷路径也在 Vault,但不要与普通订单净额结算混淆。

读完以后问自己

  • Vault 根据什么判断从池转出还是向池转入?
  • 为什么结算要使用净额而不是逐 fill 转账?
  • Vault 与 BalanceManager 的授权关系在哪里体现?

ch03-07 BalanceManager:交易账户抽象

返回本章

先抓住结构

读“BalanceManager:交易账户抽象”时先画边界。一个真实协议最容易读乱的地方,不是函数太多,而是不知道 Pool、Book、State、Vault 和 BalanceManager 各自负责哪一段。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

balance_manager.move 把交易余额从钱包 owned coin 中抽象出来。BalanceManager 内部用 BagBalanceKey<T> 保存多资产余额,并记录 owner 和 allow list。

下单需要:

  • &mut BalanceManager
  • &TradeProof
  • Pool 根据订单结果调用 Vault::settle_balance_manager
  • Vault 内部调用 balance_manager.validate_proof(trade_proof)withdraw_with_proof/deposit_with_proof

高频交易系统通常使用同一个 BalanceManager 跨多个池交易,这样可以避免每笔订单都拆分钱包 coin。生产系统必须做好 BalanceManager 的权限边界:默认 owner 可生成 proof,TradeCap 持有人也可交易,但 TradeCap 是 owned object,会受到对象并发限制。

阅读补充

BalanceManager 不是钱包余额的别名,而是 DeepBook 交易账户。它可以保存多资产余额,并通过 owner、cap 和 proof 支持授权交易;Pool/Vault 只认通过校验的证明和账户余额,不认前端登录态。

开发上应把“充值到 BalanceManager”和“用 BalanceManager 下单”拆成两个流程。前者改变账户内部余额,后者消耗或增加交易账户余额并可能创建 open order。

工程判断

  • 下单前检查 BalanceManager 余额和 TradeProof,而不是只检查 wallet coin。
  • 授权交易要记录 cap 归属和 custom owner 场景。
  • 余额展示要区分 available、open order 锁定和已结算可提取。

读完以后问自己

  • BalanceManager 与钱包地址有什么关系但为什么不等同?
  • TradeProof 在 Vault 结算时起什么作用?
  • 用户充值成功后为什么仍可能无法下单?

ch03-08 Registry:池注册、版本和权限

返回本章

先抓住结构

先把“Registry:池注册、版本和权限”放进 DeepBook 的对象图里。这里不是罗列模块,而是建立阅读顺序:入口在哪里,状态放在哪里,资金最终在哪里结算。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

RegistryRegistryInner 保存 allowed_versionspools: Bagtreasury_address。池注册使用 PoolKey { base, quote },并且 register_pool 同时检查反向交易对是否已存在,避免同一对资产出现 A/BB/A 两个独立池。

Registry::load_innerPool::load_inner 都检查当前 package version 是否在 allowed set 中。应用集成时,遇到版本禁用 abort 不应重试同一交易,而应刷新包配置、池 ID 和 SDK 常量。

阅读补充

Registry 负责回答“这个池是否被协议承认、当前版本是否可用、哪些应用或资产被授权”。它不撮合订单,但它的注册和版本检查会影响交易入口能否继续执行。

阅读 Registry 时重点看注册池、版本加载、稳定币/白名单和 authorize_app 相关动态字段。运维脚本通常会调用这些能力,但脚本只是入口,真正权限仍在 Move 代码里。

工程判断

  • 新增池或切换池前先查 Registry,而不是只保存 Pool ID。
  • 版本禁用应展示为协议维护/升级类错误,不要提示余额不足。
  • 应用授权要查动态字段和 cap,不要只看前端配置。

读完以后问自己

  • Registry 解决哪些 Pool 自身不适合解决的问题?
  • 反向交易对重复注册为什么需要检查?
  • 版本开关失败和交易参数失败如何区分?

ch03-09 DEEP token 在费用、staking、rebate 中的位置

返回本章

先抓住结构

读“DEEP token 在费用、staking、rebate 中的位置”时先画边界。一个真实协议最容易读乱的地方,不是函数太多,而是不知道 Pool、Book、State、Vault 和 BalanceManager 各自负责哪一段。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

DeepBookV3 中 DEEP 出现在三条路径:

  • 下单费用:pay_with_deep 为 true 时,pool::place_order_intdeep_price 生成 OrderDeepPriceorder_info.calculate_partial_fill_balances 计算 DEEP fee。
  • 治理和费率:state::process_create 根据 stake、交易量和 trade_params 计算 taker fee;state::process_stakeprocess_unstake 更新账户 stake 和治理投票权。
  • rebate 和销毁:state/history/governance 记录费用与 rebate,pool.move 也包含 DeepBurned 事件和 vault.withdraw_deep_to_burn 路径。

不要把 DEEP 仅理解成手续费代币。它同时影响用户费率、池治理、maker rebate 和协议统计。

阅读补充

DEEP 在 DeepBook 中不是普通展示积分。它会出现在 fee 支付选项、stake 与治理权重、rebate 历史和 burn 路径中;不同路径对应的余额方向和事件也不同。

阅读 DEEP 相关逻辑时先判断它是费用计价、实际支付资产、stake 资产还是统计字段。尤其是 pay_with_deep 打开时,订单结算会多一条 DEEP 资金线,需要与 base/quote 分开记录。

工程判断

  • 展示费用时明确 fee 是 quote 支付还是 DEEP 支付。
  • stake/rebate 统计要按 epoch 和账户维度查询 State/History。
  • 销毁路径需要追踪 Vault 提币和 burn 事件,不能只看总供应叙述。

读完以后问自己

  • pay_with_deep 会给结算增加哪条资金线?
  • stake 如何影响费用或治理相关状态?
  • rebate 和 burn 应该分别从哪些模块追踪?

ch03-10 对象借用顺序和并行边界

返回本章

先抓住结构

先把“对象借用顺序和并行边界”放进 DeepBook 的对象图里。这里不是罗列模块,而是建立阅读顺序:入口在哪里,状态放在哪里,资金最终在哪里结算。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

一次下单会 mutably borrow PoolBalanceManager。在 Pool 内部,load_inner_mut 后会修改 bookstatevault。所以同一个池内的订单天然串行化到同一个共享 Pool 对象上;不同池因为是不同共享对象,可以由 Sui 调度器并行处理。

BalanceManager 是另一个并行边界。两个交易如果同时修改同一个 BalanceManager,也会冲突;如果同一交易员为不同策略使用不同 BalanceManager,就能降低账户级对象冲突。

阅读补充

Sui 的并行执行能力要求开发者看清对象借用。对同一个 &mut Pool 的下单会争用同一个共享对象;不同用户的 BalanceManager 虽然是不同对象,但只要交易同时写同一个 Pool,就仍受池级撮合状态约束。

阅读函数签名时把参数分成 &mut&、owned value、cap/proof 四类。&mut 决定写集合和并发冲突,& 多半是只读配置或时间,owned value/cap 决定权限和资源移动。

工程判断

  • 性能分析先列 mutable shared object,不先猜测业务角色。
  • 同池交易的串行边界通常在 Pool/Book 状态,跨池交易要看是否共享 Registry 或其它对象。
  • 交易构造应避免无必要地传入 mutable 对象。

读完以后问自己

  • 为什么两个不同用户下同一个池仍可能冲突?
  • &Clock&mut Pool 对并发的影响有什么不同?
  • 如何从函数签名快速画出借用表?

ch03-11 DeepBookV2 到 V3 的关键变化

返回本章

先抓住结构

读“DeepBookV2 到 V3 的关键变化”时先画边界。一个真实协议最容易读乱的地方,不是函数太多,而是不知道 Pool、Book、State、Vault 和 BalanceManager 各自负责哪一段。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

从源码结构看,V3 的重点是把交易入口统一到 Pool,把用户资金抽象到 BalanceManager,并通过 State 把账户、历史、治理和费用组织到池内。相比只关注订单簿本身的阅读方式,V3 更强调完整交易账户:

  • 订单簿不直接保管用户余额。
  • 订单生命周期返回 OrderInfo,事件和余额计算围绕它展开。
  • 池状态使用 Versioned 控制升级可用性。
  • Vault 明确承担资产保管和闪电贷底层能力。

如果你迁移旧集成,优先检查 BalanceManager、TradeProof、订单类型常量、事件结构和 fee 字段,而不是只替换下单函数名。

阅读补充

从 V2 到 V3 的阅读重点不是“函数名变了”,而是架构职责重新拆分:用户余额通过 BalanceManager 抽象,订单生命周期集中到 OrderInfo,资产保管交给 Vault,版本控制进入 Pool/Registry 加载路径。

迁移应用时要先重画交易输入对象和事件消费逻辑。旧代码如果假设订单簿直接保管用户余额,或只监听旧事件字段,就会在 V3 中出现余额解释和订单状态回放错误。

工程判断

  • 迁移前列出旧代码依赖的余额、订单和事件假设。
  • 把 V3 的 BalanceManager 充值/授权流程纳入用户 onboarding。
  • 版本控制和 Registry 检查应成为 SDK 初始化的一部分。

读完以后问自己

  • V3 为什么引入 BalanceManager 抽象?
  • OrderInfo 对事件和余额计算有什么帮助?
  • 旧 V2 应用迁移时最容易漏掉哪两类输入对象?

ch03-12 DeepBookV3 与 Margin、Predict 的依赖关系

返回本章

先抓住结构

先把“DeepBookV3 与 Margin、Predict 的依赖关系”放进 DeepBook 的对象图里。这里不是罗列模块,而是建立阅读顺序:入口在哪里,状态放在哪里,资金最终在哪里结算。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

DeepBookV3 是现货 CLOB 和池级资产保管层。DeepBook Margin 与 Predict 是独立 package 范围内的上层协议或相邻协议,源码目录分别在 packages/deepbook_marginpackages/predict

本章讨论的 vault::FlashLoan 是 DeepBookV3 闪电贷 hot potato,不是 Margin 普通借贷债务。读 Indexer 或事件表时也要区分 DeepBookV3 的 FlashLoanBorrowed 与 Margin 的借贷事件。

阅读补充

DeepBookV3、Margin、Predict 可以共享生态和资产,但源码包和状态机需要分开阅读。spot 下单路径不应引入 Margin 借贷债务,Predict 的市场结算也不应被解释成订单簿撮合。

判断依赖关系时优先看 Move.toml、模块 import、对象类型和事件模块。只有源码中出现明确调用或共享对象关系,才把它写成依赖;否则写成相邻协议或后续章节入口。

工程判断

  • 文档中提到借贷时明确来自 Margin 还是 V3 flash loan。
  • 事件索引表要按 package/module 区分 spot、margin、predict。
  • 跨产品组合交易要分别列出每个包的对象和权限。

读完以后问自己

  • V3 flash loan 和 Margin loan 的状态生命周期有什么不同?
  • Predict 包为什么不能用 Pool/Book 流程解释?
  • 如何用源码判断两个产品是否存在真实依赖?

ch03-13 源码阅读路线

返回本章

先抓住结构

读“源码阅读路线”时先画边界。一个真实协议最容易读乱的地方,不是函数太多,而是不知道 Pool、Book、State、Vault 和 BalanceManager 各自负责哪一段。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

建议按以下顺序阅读:

  1. pool.move:先看 public 函数,理解外部调用面。
  2. book/book.move:看 Book 字段、create_ordermatch_against_bookinject_limit_order
  3. book/order_info.move:看订单状态、订单类型约束、fill 事件生成。
  4. book/order.movebook/fill.move:看 maker 订单如何变成 Fill。
  5. state/state.move:看 fill 如何转换为账户余额、费用和历史数据。
  6. vault/vault.move:看净额如何转成 BalanceManager 存取款。
  7. registry.movebalance_manager.move:补齐版本、权限和账户模型。

阅读补充

推荐路线是先读 pool.move 的 public 函数签名,再读 book.move 的撮合,再读 state.move 的费用和账户更新,再读 vault.move 的资产结算,最后读事件和 Indexer 查询。这条路线对应真实交易顺序,比按文件名 alphabet 读更有效。

每读完一个模块都写一句“它不负责什么”。例如 Book 不负责资产保管,Vault 不负责价格排序,Indexer 不负责交易执行。负面边界能帮助你在调试时快速排除错误归因。

工程判断

  • 每次跳文件都记录触发跳转的函数名或返回值。
  • 阅读笔记要保留“不负责”边界,减少架构误解。
  • 查询和 Indexer 放在最后读,因为它们解释结果,不决定交易原子性。

读完以后问自己

  • 为什么阅读路线从 Pool 开始?
  • Book、State、Vault 的阅读顺序对应交易中的哪些阶段?
  • 为什么 Indexer 不适合作为第一源码入口?

ch03-14 下单调用链练习

返回本章

先抓住结构

先把“下单调用链练习”放进 DeepBook 的对象图里。这里不是罗列模块,而是建立阅读顺序:入口在哪里,状态放在哪里,资金最终在哪里结算。

架构坐标

先把架构坐标钉在这些文件上。读这一节时不需要展开所有实现,只要看清对象分工、入口函数和后续会反复回来的状态边界。

这些入口是架构地图上的锚点。先标注每个模块负责的状态,再顺着一次下单或结算回到具体函数。

读架构

用一个限价买单推演源码路径:

  • 输入:is_bid = trueprice = 1000quantity = 5000order_type = no_restriction
  • Pool 创建 OrderInfo,记录 taker、BalanceManager、订单类型、价格、数量、DEEP fee 选项。
  • Book 生成 bid order id,并从 asks 最低价开始扫描。
  • 如果 ask price <= bid price,则 order_info.match_maker 调用 maker Order::generate_fill
  • Fill 记录 maker order id、成交价、base/quote 数量、maker/taker fee 标记。
  • 完全成交的 maker 从 asks 移除,过期 maker 也移除。
  • 如果 taker 剩余数量未完成并且不是 IOC/FOK/Post-only abort 场景,则插入 bids。
  • State 更新 maker/taker 账户,计算费用和 owed/settled。
  • Vault 在 Pool 与 BalanceManager 之间做净额结算。
  • 最后 emit OrderInfoOrderFilledOrderPlacedOrderFullyFilled 等事件。

阅读补充

这个练习建议实际写成表格:每一行是一个阶段,列出当前模块、关键函数、输入、输出、可能 abort 和事件。这样一笔订单能同时训练源码跳转、交易参数理解和失败排查。

复盘时不要只写成功路径。把 IOC/FOK/Post-only、过期 maker、部分成交、余额不足、tick/lot 不合法分别放进旁注,后续做 SDK 或前端时就能把错误提示设计得更准确。

工程判断

  • 练习输出必须包含控制流、资金流和事件流三列。
  • 每个阶段都记录可能 abort 的原因,尤其是参数和权限检查。
  • 部分成交场景要说明剩余数量是插入订单簿还是被订单类型取消。

读完以后问自己

  • 限价买单从 Pool 到 Vault 至少经过哪些模块?
  • 部分成交后 OrderInfo 为什么还要继续参与 State 处理?
  • 哪些订单类型会改变“剩余数量是否挂单”的结果?

ch04 Pool、Book 与撮合源码精读

本章目标

  • 读懂 Pool 下单入口、Book 撮合循环、Order maker 状态、Fill 成交结果和 OrderInfo taker 生命周期。
  • 能解释 bid、ask、price level、queue priority 如何编码到 order_idBigVector<Order> 中。
  • 能推演限价单、市价单、IOC、FOK、post-only、自成交策略、取消和修改订单的状态变化。
  • 能把链上事件和源码中的状态字段对应起来,服务前端订单查询和 indexer 落库。

本章学习阶梯

  • L2 先用三档订单簿手工推演 bid、ask、price level 和 fill。
  • L3 再读订单 ID、撮合循环、剩余挂单和事件生成。
  • L4 能解释 post-only、IOC、FOK、自成交策略的失败边界。
  • L5 能把撮合结果还原成前端订单状态和 Indexer 数据。

关键定义卡片

撮合章先分清 OrderOrderInfoFill

public struct Order has drop, store {
    balance_manager_id: ID,
    order_id: u128,
    client_order_id: u64,
    quantity: u64,
    filled_quantity: u64,
    fee_is_deep: bool,
    order_deep_price: OrderDeepPrice,
    epoch: u64,
    status: u8,
    expire_timestamp: u64,
}

Order 是可以留在订单簿里的 maker 状态。它有 filled_quantitystatusexpire_timestamp,所以取消、过期、部分成交都能从这里更新。

public struct OrderInfo has copy, drop, store {
    pool_id: ID,
    order_id: u128,
    balance_manager_id: ID,
    client_order_id: u64,
    trader: address,
    order_type: u8,
    self_matching_option: u8,
    price: u64,
    is_bid: bool,
    original_quantity: u64,
    executed_quantity: u64,
    cumulative_quote_quantity: u64,
    fills: vector<Fill>,
    status: u8,
    market_order: bool,
    order_inserted: bool,
    timestamp: u64,
}

OrderInfo 是一次新订单的生命周期结果。完全吃单的市价单可能只返回 OrderInfo,不会插入簿成为 Order。这也是前端和 Indexer 容易写错的地方。

public struct Fill has copy, drop, store {
    maker_order_id: u128,
    execution_price: u64,
    balance_manager_id: ID,
    base_quantity: u64,
    quote_quantity: u64,
    taker_is_bid: bool,
    taker_fee: u64,
    maker_fee: u64,
}

Fill 是一笔 taker 与一个 maker 的成交切片,不是一整笔订单。一个 taker 订单可能产生多个 Fill

源码地图

小节目录

本章代码

  • code/s01-orderbook-simulator/:用 TypeScript 实现最小订单簿模拟器。
  • code/s02-match-trace/:输入订单列表,输出逐步撮合日志。
  • code/s03-order-query/:查询链上订单并与本地模拟排序规则对比。

Move 高阶穿插点

  • 撮合源码要按“输入校验、订单构造、撮合循环、剩余处理、事件输出、资金结算”六段读。
  • 订单簿里的整数不要急着换成小数显示;先保留链上原始单位,最后一层 UI 再格式化。
  • OrderInfoOrder 要分开理解:前者描述本次交易结果,后者描述可留在簿上的状态。

常见错误

  • 错误:把市价单当成没有价格。源码中市价买单使用 max price,市价卖单使用 min price,并强制 IOC。
  • 错误:用 JavaScript number 保存 u128 order id。必须使用字符串或 BigInt。
  • 错误:认为部分成交普通限价单的状态一定是 live。OrderInfo 可是 partially_filled,同时剩余部分以 Order 进入 Book。
  • 错误:取消订单只删 Book。实际还要更新 account open orders、计算 refund、通过 Vault 结算。
  • 错误:只监听 OrderFilled。订单生命周期还需要 OrderInfoOrderPlacedOrderFullyFilledOrderCanceledOrderExpired

本章检查清单

  • 能解释 encoded order id 的 bit 布局。
  • 能说明 bid/ask 分别从哪一侧、哪个价格方向开始撮合。
  • 能写出限价单进入 Book 的完整函数链。
  • 能解释 Fill 中 expired/completed 的不同含义。
  • 能推演 IOC、FOK、post-only 和自成交策略的行为。
  • 能说明取消订单如何退还锁定资产。

进阶练习

  • 练习 1:用 BigInt 写一个 decodeOrderId,输入链上 order id 后输出 side、price、sequence。
  • 练习 2:构造一个 maker 过期场景,解释为什么 Fill.expired = true 但它仍会参与移除订单和释放锁定资产。
  • 练习 3:设计一个包含 4 个 maker 的订单簿,分别推演普通限价、IOC 和 FOK taker 的最终状态。

ch04-01 bid、ask、price level、queue priority

返回本章

先看订单现场

读这一节时,不要从函数名开始。先问一笔订单走到“bid、ask、price level、queue priority”这个环节时,排序、成交、挂单、取消或退款中哪一个状态会发生变化。

源码入口

把订单走一遍

bid 是买 base、付 quote;ask 是卖 base、收 quote。DeepBook 的订单数量 quantity 始终按 base asset 计量,价格表示每单位 base 对应多少 quote。

Book 内有两侧:

bids: BigVector<Order>,
asks: BigVector<Order>,
next_bid_order_id: u64,
next_ask_order_id: u64,

book::match_against_book 对买单从 asks 的最低价开始扫,对卖单从 bids 的最高价开始扫。这就是 CLOB 的价格优先。相同价格下,get_order_id 让 bid 的序号递减、ask 的序号递增,再由 utils::encode_order_id 编入低 64 位,形成可排序 key。队列优先级因此由编码后的 order_idBigVector 顺序共同决定。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

查询侧要保持 order_id 为字符串或 BigInt。packages/deepbook/sources/order_query.move 返回链上排序后的订单视图,前端聚合深度时只能按 price level 合并数量,不能重新用 JavaScript number 排序 u128。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • bid、ask、price level、queue priority 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-02 Order 结构和生命周期

返回本章

先看订单现场

先把“Order 结构和生命周期”放进一笔订单里看。用户看到的是买卖按钮,链上真正发生的是 Pool 接住入口、Book 决定撮合、OrderInfo 带着执行结果回到资金结算。

源码入口

关键定义

Order 是真正会留在订单簿里的 maker 状态:

public struct Order has drop, store {
    balance_manager_id: ID,
    order_id: u128,
    client_order_id: u64,
    quantity: u64,
    filled_quantity: u64,
    fee_is_deep: bool,
    order_deep_price: OrderDeepPrice,
    epoch: u64,
    status: u8,
    expire_timestamp: u64,
}

字段要按三组理解:

  • 归属字段:balance_manager_idclient_order_id
  • 撮合字段:order_idquantityfilled_quantitystatusexpire_timestamp
  • 费用字段:fee_is_deeporder_deep_priceepoch

Order 没有 trader: address 字段,因为订单归属以 BalanceManager 为中心。事件中看到的 trader,是在取消、成交或事件生成时补出来的上下文,不是订单簿长期状态。

把订单走一遍

order.moveOrder 是挂在订单簿里的 maker 状态,字段包括:

  • balance_manager_id:订单归属账户。
  • order_id:编码后的 u128 订单 ID。
  • client_order_id:调用方自定义 ID。
  • quantityfilled_quantity:原始数量和已成交数量。
  • fee_is_deeporder_deep_price:maker 费用计算所需数据。
  • epoch:用于历史 maker fee。
  • status:live、partially_filled、filled、canceled、expired。
  • expire_timestamp:订单过期时间。

生命周期:

new OrderInfo
  -> match maker orders
  -> if remaining and allowed, to_order
  -> Book injects Order
  -> later generate_fill / modify / cancel / expire
  -> State removes open order and settles balances

Order 本身不保存 trader address,只保存 BalanceManager ID。事件里的 trader 由 taker 或取消调用者上下文补充。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

边界条件主要来自价格是否穿透、数量是否满足最小 lot、maker 是否过期,以及 taker 执行策略是否允许剩余挂单。手工推演时把每一次 Fill 后的 maker 剩余量和 taker 剩余量写出来,最容易发现状态跳转错误。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • Order 结构和生命周期 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-03 订单 ID 的生成、排序和查询

返回本章

先看订单现场

读这一节时,不要从函数名开始。先问一笔订单走到“订单 ID 的生成、排序和查询”这个环节时,排序、成交、挂单、取消或退款中哪一个状态会发生变化。

源码入口

把订单走一遍

utils::encode_order_id(is_bid, price, order_id) 的布局:

  • 第 1 bit:bid 为 0,ask 为 1。
  • 接下来 63 bits:price。
  • 最后 64 bits:同侧递增或递减序号。

bid 不加最高位,ask 加 1u128 << 127decode_order_id 返回 (is_bid, price, order_id)

Book::create_order 先调用 get_order_id(order_info.is_bid()),再编码到 OrderInfoorder_query::iter_orders 利用这个排序特性分页:bids 从高到低迭代,asks 从低到高迭代。

开发实践:前端展示订单 ID 时应保留 u128 字符串,不要转 JavaScript number。需要解析价格时使用 BigInt 按同样位布局解码。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

查询侧要保持 order_id 为字符串或 BigInt。packages/deepbook/sources/order_query.move 返回链上排序后的订单视图,前端聚合深度时只能按 price level 合并数量,不能重新用 JavaScript number 排序 u128。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • 订单 ID 的生成、排序和查询 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-04 Book 如何保存买卖两侧订单

返回本章

先看订单现场

先把“Book 如何保存买卖两侧订单”放进一笔订单里看。用户看到的是买卖按钮,链上真正发生的是 Pool 接住入口、Book 决定撮合、OrderInfo 带着执行结果回到资金结算。

源码入口

关键定义

Book 是订单簿真正保存排序状态的结构体。Pool 负责权限、资金和版本门禁,Book 只关心价格层、订单序号和两侧订单容器。

public struct Book has store {
    tick_size: u64,
    lot_size: u64,
    min_size: u64,
    bids: BigVector<Order>,
    asks: BigVector<Order>,
    next_bid_order_id: u64,
    next_ask_order_id: u64,
}

这里最容易误读的是 next_bid_order_idnext_ask_order_id。它们不是前端展示用的自增整数,而是参与 u128 order_id 编码的序号来源。bid 和 ask 的排序方向不同,最终都被压进一个可比较的 order id 中,所以应用层必须把 order_id 当作链上排序键,而不是普通数据库自增主键。

把订单走一遍

Book 使用 BigVector<Order> 保存 bids 和 asks,而不是普通 vector。它需要支持按 encoded order id 查找、插入、删除、区间扫描和分页。

关键函数:

  • empty(tick_size, lot_size, min_size, ctx):初始化两侧 BigVector 和订单序号。
  • book_side_mut(order_id):通过解码 order id 判断订单在哪一侧。
  • cancel_order(order_id):从对应侧移除订单并返回 Order
  • modify_order(order_id, new_quantity, timestamp):降低订单数量,不能增加数量。
  • get_level2_range_and_ticks:按价格范围聚合 level2 深度。

Book 不维护账户开放订单列表;那部分在 state/account.move。所以取消订单时 Pool 必须同时更新 Book 和 State。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

边界条件主要来自价格是否穿透、数量是否满足最小 lot、maker 是否过期,以及 taker 执行策略是否允许剩余挂单。手工推演时把每一次 Fill 后的 maker 剩余量和 taker 剩余量写出来,最容易发现状态跳转错误。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • Book 如何保存买卖两侧订单 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-05 限价单进入撮合引擎的完整路径

返回本章

先看订单现场

读这一节时,不要从函数名开始。先问一笔订单走到“限价单进入撮合引擎的完整路径”这个环节时,排序、成交、挂单、取消或退款中哪一个状态会发生变化。

源码入口

关键定义

限价单的入口签名本身就是完整参数表:

public fun place_limit_order<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    balance_manager: &mut BalanceManager,
    trade_proof: &TradeProof,
    client_order_id: u64,
    order_type: u8,
    self_matching_option: u8,
    price: u64,
    quantity: u64,
    is_bid: bool,
    pay_with_deep: bool,
    expire_timestamp: u64,
    clock: &Clock,
    ctx: &TxContext,
): OrderInfo

读这个签名时,不要只看 pricequantityselfbalance_manager 都是 &mut,说明一次下单会同时改变池状态和用户交易账户。trade_proof 是权限边界。order_typeself_matching_option 决定订单是否可以剩余挂单、是否必须完全成交、遇到自成交时取消哪一侧。返回值 OrderInfo 是本次执行结果,不能直接等同于订单簿上的 Order

把订单走一遍

限价单入口是 pool::place_limit_order,参数包括 BalanceManager、TradeProof、client order id、order type、self matching option、price、quantity、side、pay_with_deep、expire timestamp、Clock。

内部 place_order_int 做四件事:

  1. 更新 EWMA 状态并读取池的 PoolInner
  2. 根据 pay_with_deep 生成 OrderDeepPrice
  3. 创建 OrderInfo 并调用 book.create_order
  4. 调用 state.process_createvault.settle_balance_manager 完成账户与资产结算。

book::create_order 的顺序非常重要:

validate_inputs
set_order_id
match_against_book
assert_execution
inject_limit_order
emit_order_placed

如果订单完全成交、IOC 取消剩余、FOK 不满足而 abort,或者 post-only 发生 crossing abort,它不会被插入订单簿。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

函数调用链可以按“入口参数校验 -> OrderInfo::validate_inputs -> book::match_against_book -> OrderInfo::assert_execution”阅读。资金路径不是在 Book 内直接转 coin,而是由成交结果写入账户应收应付,后续再交给 Vault 和 BalanceManager 结算。

Move 技巧:撮合模块里看不到大量 Coin<T> 转移是正常的。高质量协议会把“计算成交结果”和“移动资产”分开,前者生成 fill 和应收应付,后者再由 VaultBalanceManager 统一结算。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • 限价单进入撮合引擎的完整路径 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-06 市价单和吃单资金检查

返回本章

先看订单现场

交易界面里的“市价买入”看起来像一个简单按钮:按现在盘口立刻成交。但在 DeepBook 里,它不是一种绕过价格系统的特殊订单。更准确地说,它是一笔被推到极端价格的限价单,再用 IOC 限制剩余数量。

这个设计很重要。协议保证订单不会留下意外挂单,但它不会替应用承担滑点保护。交易终端、做市机器人或聚合器如果不先估算输出,用户仍然可能在很薄的订单簿上吃到很差的价格。

源码入口

这一节只需要抓住三处:

关键定义

市价单入口本身很短,它把价格改成极端值,并固定使用 IOC。真正的撮合与资金结算仍然走同一条内部路径。

public fun place_market_order<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    balance_manager: &mut BalanceManager,
    trade_proof: &TradeProof,
    client_order_id: u64,
    self_matching_option: u8,
    quantity: u64,
    is_bid: bool,
    pay_with_deep: bool,
    clock: &Clock,
    ctx: &TxContext,
): OrderInfo {
    self.place_order_int(
        balance_manager,
        trade_proof,
        client_order_id,
        constants::immediate_or_cancel(),
        self_matching_option,
        if (is_bid) constants::max_price() else constants::min_price(),
        quantity,
        is_bid,
        pay_with_deep,
        clock.timestamp_ms(),
        clock,
        true,
        ctx,
    )
}

这段代码说明一个重要设计:DeepBook 没有“无限滑点”的市价单类型。它用 bid 最大价或 ask 最小价把订单推入限价撮合路径,然后用 IOC 阻止剩余数量挂单。因此应用必须自己做 quote、深度检查和最小输出保护。

把订单走一遍

pool::place_market_order 没有另写一套市价撮合引擎。它把参数整理好后直接进入 place_order_int

  • bid 市价单使用 constants::max_price()
  • ask 市价单使用 constants::min_price()
  • order type 固定为 immediate_or_cancel()
  • market_order = true,并把 expire timestamp 设为当前时间。

这几个参数合在一起,得到的行为才像用户理解中的“市价单”:尽可能按当前订单簿成交,剩余部分不进入 book。

order_info::validate_inputs 对 market order 会跳过普通限价价格校验,但仍然禁止 post-only。资金检查不在 Book 里发生。Book 只产生成交;买方 owed quote 和 fee、卖方 owed base 和 fee 会进入 OrderInfo 和 account/vault 结算路径。如果 BalanceManager 余额不足,最后仍会在 proof 取款或结算阶段 abort。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

生产应用不能只依赖 max/min price。下单前应先用 book::get_quantity_out 或 SDK quote 能力估算成交量、手续费和最差可接受输出;提交后再用事件或订单查询确认实际成交。

交易实现提醒

  • 报价预估不是体验优化,而是市价单的安全前置条件。
  • 前端展示“预计成交价”时,要同时展示深度不足和 fee 资产,尤其是 pay_with_deep = true 的情况。
  • 事件监听要按 maker/taker 方向解释 base、quote,不能只看 is_bid 一个字段。

动手检查

  • 用一个“买入 10 SUI”的例子推演:bid 市价单最终传入的 price 是什么?
  • 如果订单簿只够成交 6 SUI,剩余 4 SUI 会进入 book 吗?
  • 如果 dry run 失败,先排查滑点、余额、DEEP fee,还是 self matching option?

ch04-07 post-only、IOC、FOK、自成交策略

返回本章

先看订单现场

读这一节时,不要从函数名开始。先问一笔订单走到“post-only、IOC、FOK、自成交策略”这个环节时,排序、成交、挂单、取消或退款中哪一个状态会发生变化。

源码入口

关键定义

订单执行策略和自成交策略都是 u8 常量函数。SDK、前端和策略程序最好通过官方 SDK 或同名常量映射生成参数,不要在业务代码里散落裸数字。

public fun no_restriction(): u8 {
    NO_RESTRICTION
}

public fun immediate_or_cancel(): u8 {
    IMMEDIATE_OR_CANCEL
}

public fun fill_or_kill(): u8 {
    FILL_OR_KILL
}

public fun post_only(): u8 {
    POST_ONLY
}

public fun self_matching_allowed(): u8 {
    SELF_MATCHING_ALLOWED
}

public fun cancel_taker(): u8 {
    CANCEL_TAKER
}

public fun cancel_maker(): u8 {
    CANCEL_MAKER
}

Move 里用常量函数而不是枚举,是为了让入口参数保持简单。但这也意味着应用层需要主动做参数约束:post_only 和市价单不兼容,fill_or_kill 需要先用订单簿估算完整成交概率,自成交策略需要和同一个 BalanceManager 下的多策略机器人配合。

把订单走一遍

订单类型在 constants.move 中包括:

  • no_restriction():可吃单,剩余可挂单。
  • immediate_or_cancel():可吃单,剩余立即取消,不挂单。
  • fill_or_kill():必须完全成交,否则 abort。
  • post_only():不得与现有订单成交,否则 abort。

order_info::assert_execution 负责检查这些限制。post-only 如果 executed_quantity > 0 会 abort;FOK 如果未完全成交会 abort;IOC 会把剩余数量标成 canceled 并返回,不插入订单簿。

自成交策略在 order_info::match_maker

  • cancel_taker():如果 maker 和 taker 使用同一个 BalanceManager,直接 abort。
  • cancel_maker():如果同账户相遇,生成 expired/canceled maker fill,并移除 maker。
  • 默认允许自成交。

机器人和做市系统必须明确设置 self matching option,否则同一个 BalanceManager 下的多个策略可能互相成交。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

函数调用链可以按“入口参数校验 -> OrderInfo::validate_inputs -> book::match_against_book -> OrderInfo::assert_execution”阅读。资金路径不是在 Book 内直接转 coin,而是由成交结果写入账户应收应付,后续再交给 Vault 和 BalanceManager 结算。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • post-only、IOC、FOK、自成交策略 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-08 Fill 如何表示一次成交

返回本章

先看订单现场

先把“Fill 如何表示一次成交”放进一笔订单里看。用户看到的是买卖按钮,链上真正发生的是 Pool 接住入口、Book 决定撮合、OrderInfo 带着执行结果回到资金结算。

源码入口

关键定义

Fill 是一次 taker 和一个 maker 的成交切片:

public struct Fill has copy, drop, store {
    maker_order_id: u128,
    maker_client_order_id: u64,
    execution_price: u64,
    balance_manager_id: ID,
    expired: bool,
    completed: bool,
    original_maker_quantity: u64,
    base_quantity: u64,
    quote_quantity: u64,
    taker_is_bid: bool,
    maker_epoch: u64,
    maker_deep_price: OrderDeepPrice,
    taker_fee: u64,
    taker_fee_is_deep: bool,
    maker_fee: u64,
    maker_fee_is_deep: bool,
}

这里的 balance_manager_id 是 maker 的 manager,不是 taker。taker_is_bid 决定结算方向:taker 买入时 maker 卖出 base、收 quote;taker 卖出时 maker 买入 base、付 quote。expiredcompleted 不是 UI 状态装饰,它们决定 Book 是否移除 maker order,以及 State 如何释放或结算锁定资产。

把订单走一遍

fill.moveFill 是 taker 与一个 maker order 的匹配结果。字段包括 maker order id、maker client order id、execution price、maker BalanceManager、是否 expired、是否 completed、原始 maker quantity、本次 base/quote 数量、taker side、maker epoch、maker deep price、maker/taker fee。

Order::generate_fill 负责创建 Fill:

  • 如果 maker 过期或被 cancel_maker 策略处理,expired = true,base/quote 数量表示要释放的 maker 剩余锁定量。
  • 如果正常成交,更新 maker filled_quantity,状态变为 partially filled 或 filled。
  • completed 表示 maker 是否完全成交,Book 后续用它决定是否 remove。

Fill::get_settled_maker_quantities 根据 taker side 判断 maker 应收到 base 还是 quote。比如 taker 是 bid,maker 是 ask,正常成交后 maker 收 quote。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

事件阅读时要把 OrderPlacedOrderFilledOrderFullyFilledOrderCanceledOrderExpired 串成生命周期。取消和过期还会触发锁定资产释放,退款由 Order 计算后进入结算路径。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • Fill 如何表示一次成交 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-09 OrderInfo 如何生成订单和成交事件

返回本章

先看订单现场

读这一节时,不要从函数名开始。先问一笔订单走到“OrderInfo 如何生成订单和成交事件”这个环节时,排序、成交、挂单、取消或退款中哪一个状态会发生变化。

源码入口

关键定义

OrderInfo 是一次新订单从进入撮合到返回结果的临时对象:

public struct OrderInfo has copy, drop, store {
    pool_id: ID,
    order_id: u128,
    balance_manager_id: ID,
    client_order_id: u64,
    trader: address,
    order_type: u8,
    self_matching_option: u8,
    price: u64,
    is_bid: bool,
    original_quantity: u64,
    executed_quantity: u64,
    cumulative_quote_quantity: u64,
    fills: vector<Fill>,
    fee_is_deep: bool,
    paid_fees: u64,
    maker_fees: u64,
    epoch: u64,
    status: u8,
    market_order: bool,
    fill_limit_reached: bool,
    order_inserted: bool,
    timestamp: u64,
}

这组字段把“订单意图”和“执行结果”放在同一个结构里。original_quantity 是用户输入,executed_quantity 是成交 base 数量,cumulative_quote_quantity 是成交 quote 总量,fills 保存每个 maker 切片。order_inserted 很关键:它告诉你这笔订单是否最终进入订单簿。完全吃单、IOC 剩余取消或 FOK 失败,都不应在前端显示成一个 live maker order。

成交事件也直接来自 OrderInfo 中的 fill:

public struct OrderFilled has copy, drop, store {
    pool_id: ID,
    maker_order_id: u128,
    taker_order_id: u128,
    price: u64,
    taker_is_bid: bool,
    taker_fee: u64,
    maker_fee: u64,
    base_quantity: u64,
    quote_quantity: u64,
    maker_balance_manager_id: ID,
    taker_balance_manager_id: ID,
    timestamp: u64,
}

把订单走一遍

OrderInfo 是一次 taker 订单的完整执行记录,也是事件源。它会 emit:

  • 自身 OrderInfo 事件:包含订单状态、累计成交、fills、费用等。
  • OrderFilled:每个未过期 fill 对应一条成交事件。
  • OrderPlaced:剩余数量进入订单簿。
  • OrderFullyFilled:taker 或 maker 完全成交。
  • OrderExpired 或 maker cancel 事件:处理过期 maker 或 cancel maker 自成交策略。

事件生成发生在 Pool 的 place_order_int 末尾:

order_info.emit_order_info()
order_info.emit_orders_filled(timestamp)
order_info.emit_order_fully_filled_if_filled(timestamp)

Indexer 不应只依赖 OrderFilled 判断订单最终状态,还需要消费 OrderInfoOrderPlacedOrderFullyFilledOrderCanceledOrderExpired

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

事件阅读时要把 OrderPlacedOrderFilledOrderFullyFilledOrderCanceledOrderExpired 串成生命周期。取消和过期还会触发锁定资产释放,退款由 Order 计算后进入结算路径。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • OrderInfo 如何生成订单和成交事件 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-10 部分成交、完全成交、剩余挂单

返回本章

先看订单现场

先把“部分成交、完全成交、剩余挂单”放进一笔订单里看。用户看到的是买卖按钮,链上真正发生的是 Pool 接住入口、Book 决定撮合、OrderInfo 带着执行结果回到资金结算。

源码入口

把订单走一遍

order_info::match_maker 每撮合一个 maker:

  • 调用 maker.generate_fill(...)
  • 如果 fill 未过期,则增加 taker executed_quantitycumulative_quote_quantity
  • taker 状态先设为 partially filled;如果 remaining 为 0,则设为 filled。

book::match_against_book 撮合后会移除 expired 或 completed maker。然后 assert_execution 判断 taker 是否终止:

  • 完全成交:状态 filled,不挂单。
  • IOC:剩余取消,不挂单。
  • fill limit reached:不继续扫描,函数返回 true,不挂单。
  • 普通限价单仍有剩余:inject_limit_order,状态 live/partially filled,order_inserted = true

状态变化表:

场景taker executedtaker statusmaker status是否插入 taker
完全成交original_quantityfilledfilled 或 partially_filled
部分成交普通限价小于 originalpartially_filledfilled/partially_filled
IOC 部分成交小于 originalcanceledfilled/partially_filled
post-only crossing大于 0 前 abortabort回滚
FOK 不完全成交小于 original 前 abortabort回滚

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

事件阅读时要把 OrderPlacedOrderFilledOrderFullyFilledOrderCanceledOrderExpired 串成生命周期。取消和过期还会触发锁定资产释放,退款由 Order 计算后进入结算路径。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • 部分成交、完全成交、剩余挂单 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-11 取消单和批量取消路径

返回本章

先看订单现场

读这一节时,不要从函数名开始。先问一笔订单走到“取消单和批量取消路径”这个环节时,排序、成交、挂单、取消或退款中哪一个状态会发生变化。

源码入口

关键定义

严格撤单入口会先从 Book 移除订单,再检查订单归属。批量严格撤单只是循环调用单笔撤单,因此任何一笔失败都会让整笔交易回滚。

public fun cancel_order<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    balance_manager: &mut BalanceManager,
    trade_proof: &TradeProof,
    order_id: u128,
    clock: &Clock,
    ctx: &TxContext,
) {
    let self = self.load_inner_mut();
    let mut order = self.book.cancel_order(order_id);
    assert!(order.balance_manager_id() == balance_manager.id(), EInvalidOrderBalanceManager);
    let (settled, owed) = self
        .state
        .process_cancel(&mut order, balance_manager.id(), self.pool_id, ctx);
    self.vault.settle_balance_manager(settled, owed, balance_manager, trade_proof);

    order.emit_order_canceled(self.pool_id, ctx.sender(), clock.timestamp_ms());
}

public fun cancel_orders<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    balance_manager: &mut BalanceManager,
    trade_proof: &TradeProof,
    order_ids: vector<u128>,
    clock: &Clock,
    ctx: &TxContext,
) {
    let mut i = 0;
    let num_orders = order_ids.length();
    while (i < num_orders) {
        let order_id = order_ids[i];
        self.cancel_order(balance_manager, trade_proof, order_id, clock, ctx);
        i = i + 1;
    }
}

读这段代码要注意“状态顺序”和“交易原子性”是两件事。函数体里先从 Book cancel,再做 owner 校验;但如果校验失败,Move 交易整体 abort,前面的状态写入不会落链。批量取消也一样,适合“全部必须成功”的管理操作;交易前端更常用 live/no-op 版本来处理已成交或已取消的订单。

把订单走一遍

取消入口在 pool.move

  • cancel_order:必须成功取消指定订单。
  • cancel_orders:逐个调用 cancel_order,任一失败会导致整笔交易失败。
  • cancel_live_order:如果订单不在该 BalanceManager 的 open orders 中则 no-op。
  • cancel_live_orders:批量 no-op 版本。
  • cancel_all_orders:从账户 open orders 取全部订单并取消。

核心链路:

pool::cancel_order
  -> book::cancel_order
  -> assert order.balance_manager_id == balance_manager.id
  -> state::process_cancel
    -> order.set_canceled
    -> order.calculate_cancel_refund
    -> account.remove_order
    -> account.settle
  -> vault::settle_balance_manager
  -> order.emit_order_canceled

修改订单 modify_order 只能降低数量。它调用 book::modify_order,再通过 state::process_modify 退回减少部分对应的锁定资产和 maker fee。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

事件阅读时要把 OrderPlacedOrderFilledOrderFullyFilledOrderCanceledOrderExpired 串成生命周期。取消和过期还会触发锁定资产释放,退款由 Order 计算后进入结算路径。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • 取消单和批量取消路径 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-12 order query 如何服务前端查询

返回本章

先看订单现场

先把“order query 如何服务前端查询”放进一笔订单里看。用户看到的是买卖按钮,链上真正发生的是 Pool 接住入口、Book 决定撮合、OrderInfo 带着执行结果回到资金结算。

源码入口

关键定义

order_query.move 给前端和 indexer 一个轻量分页视图。它返回的是 Order 副本集合,而不是把整个 Book 暴露给调用方。

public struct OrderPage has drop {
    orders: vector<Order>,
    has_next_page: bool,
}

public fun iter_orders<BaseAsset, QuoteAsset>(
    self: &Pool<BaseAsset, QuoteAsset>,
    start_order_id: Option<u128>,
    end_order_id: Option<u128>,
    min_expire_timestamp: Option<u64>,
    limit: u64,
    bids: bool,
): OrderPage

这里的 start_order_idend_order_id 不是数据库 offset,而是订单簿排序键。bids 按从高到低遍历,asks 按从低到高遍历;如果前端把它们转成 JavaScript number,分页游标和排序都会在大整数区间失真。

把订单走一遍

order_query.moveiter_orders 返回 OrderPage { orders, has_next_page }。调用方可传:

  • start_order_id
  • end_order_id
  • min_expire_timestamp
  • limit
  • bids

bids 默认范围是 [0, 1 << 127),asks 默认范围是 [1 << 127, max_u128]。函数会跳过 expire_timestamp < min_expire 的订单。

前端展示深度时,优先使用聚合 level2 查询;展示个人订单时,需要结合账户 open orders、BalanceManager ID 和 Order 详情。不要仅通过事件重建实时订单簿,除非后端 indexer 已处理重组、去重和补偿扫描。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

查询侧要保持 order_id 为字符串或 BigInt。packages/deepbook/sources/order_query.move 返回链上排序后的订单视图,前端聚合深度时只能按 price level 合并数量,不能重新用 JavaScript number 排序 u128。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • order query 如何服务前端查询 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-13 撮合边界条件阅读方法

返回本章

先看订单现场

读这一节时,不要从函数名开始。先问一笔订单走到“撮合边界条件阅读方法”这个环节时,排序、成交、挂单、取消或退款中哪一个状态会发生变化。

源码入口

把订单走一遍

读撮合边界时优先从 abort code 和状态判断入手:

  • order_info::validate_inputs:价格范围、tick size、min size、lot size、过期时间、订单类型。
  • order_info::assert_execution:post-only、FOK、IOC。
  • order_info::match_maker:价格是否 crossing、自成交策略。
  • book::match_against_bookmax_fills 限制和 maker 移除。
  • order::modify:新数量必须大于已成交且小于原数量。
  • order::calculate_cancel_refund:取消或修改时退还未成交数量和 maker fee。

测试设计应覆盖:空簿市价单、刚好 min size、非 lot multiple、post-only crossing、FOK 部分成交、IOC 剩余取消、maker 过期、自成交 cancel taker、cancel maker、达到 max fills。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

边界条件主要来自价格是否穿透、数量是否满足最小 lot、maker 是否过期,以及 taker 执行策略是否允许剩余挂单。手工推演时把每一次 Fill 后的 maker 剩余量和 taker 剩余量写出来,最容易发现状态跳转错误。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • 撮合边界条件阅读方法 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch04-14 三档订单簿手工推演

返回本章

先看订单现场

先把“三档订单簿手工推演”放进一笔订单里看。用户看到的是买卖按钮,链上真正发生的是 Pool 接住入口、Book 决定撮合、OrderInfo 带着执行结果回到资金结算。

源码入口

把订单走一遍

假设 asks:

ask pricequantitymaker
1005A
1015B
1035C

输入 taker bid:price = 101quantity = 12、普通限价。

推演:

  1. 扫最低 ask 100,成交 5,累计 base=5、quote=500,maker A completed,从 asks 移除。
  2. 扫 ask 101,成交 5,累计 base=10、quote=1005,maker B completed,从 asks 移除。
  3. 下一个 ask 103 超过 taker price,停止。
  4. taker 剩余 2,如果满足 lot/min 约束且不是 IOC/FOK/post-only 终止,则作为 bid price 101 插入 bids。
  5. State 计算 taker 买入 10 base 应付 1005 quote 加 taker fee,剩余挂单锁定 2 * 101 quote 加 maker fee。
  6. Vault 从 taker BalanceManager 扣 quote/DEEP,把 maker 应收 quote 记入对应 BalanceManager 或 settled balances。

如果同一订单改为 IOC,第 4 步不会插入 bids,剩余 2 被取消。如果改为 FOK,因为没有完全成交 12,会 abort,前两笔成交也回滚。

源码旁白:撮合相关小节都从 packages/deepbook/sources/pool.move 的 public 入口进入,再沿 packages/deepbook/sources/book/book.move 追踪撮合、插入或取消。读的时候把 taker 的 OrderInfo 和 maker 的 Order 分开:前者是本次执行结果,后者才是可能继续留在订单簿里的状态。

边界条件主要来自价格是否穿透、数量是否满足最小 lot、maker 是否过期,以及 taker 执行策略是否允许剩余挂单。手工推演时把每一次 Fill 后的 maker 剩余量和 taker 剩余量写出来,最容易发现状态跳转错误。

交易实现提醒

  • u128 + BigInt 思维处理 order_id、price、quantity,避免前端精度丢失。
  • 先判断订单会进入撮合、直接失败、完全吃单还是剩余挂单,再设计事件监听和 UI 状态。
  • 读取 FillOrderInfo 时同时记录 maker/taker 方向,避免把 base、quote 的应收应付方向写反。

动手检查

  • 三档订单簿手工推演 依赖哪一个 pool.move 入口,下一跳进入哪个 book/* 函数?
  • 成功路径会产生哪些订单事件,失败时最可能命中输入、执行策略还是余额相关校验?
  • 如果前端或 indexer 展示本节数据,哪些字段必须保持链上原始整数格式?

ch05 BalanceManager、Vault、费用与治理

本章目标

读完本章后,你应该能把一笔 DeepBookV3 交易拆成三层资金路径:用户资产先进入 BalanceManager,撮合产生的应收应付记录在 Account,最后由 VaultBalanceManager 做实际资产划转。你还应该能解释 maker fee、taker fee、rebate、DEEP staking 和治理参数之间的关系,并能在应用侧提前检查余额、版本、权限和常见 abort code。

本章学习阶梯

  • L2 先区分钱包余额、manager 余额、locked balance 和 settled amounts。
  • L3 读 BalanceManagerVault、费用、staking 和治理源码。
  • L4 能设计 owner/trader/cap/proof 的权限模型。
  • L5 能为交易终端或做市系统设计资金安全检查。

关键定义卡片

BalanceManager 是本章主角:

public struct BalanceManager has key, store {
    id: UID,
    owner: address,
    balances: Bag,
    allow_listed: VecSet<ID>,
}

public struct TradeProof has drop {
    balance_manager_id: ID,
    trader: address,
}

balances 不是钱包余额,而是协议内部余额。allow_listed 保存被授权的 cap ID。TradeProof 是交易内权限证明,说明调用者可以代表这个 manager 交易。它有 drop,所以不会成为长期权限对象。

关键权限对象:

public struct TradeCap has key, store {
    id: UID,
    balance_manager_id: ID,
}

public struct DepositCap has key, store {
    id: UID,
    balance_manager_id: ID,
}

public struct WithdrawCap has key, store {
    id: UID,
    balance_manager_id: ID,
}

这三个 cap 把交易、存入、取出拆开。做市机器人通常只应拿 TradeCap,不应该拿 WithdrawCap。读到任何下单失败时,先问三个问题:manager 里是否有余额,proof 是否指向这个 manager,cap 是否仍在 allow list。

源码地图

  • packages/deepbook/sources/balance_manager.move:用户资金账户、owner、cap、TradeProof、存取款事件。
  • packages/deepbook/sources/vault/vault.move:池子资产金库、交易结算、闪电贷底层借还。
  • packages/deepbook/sources/state/account.move:每个 BalanceManager 在池子内的订单、成交、stake、rebate、settled/owed 状态。
  • packages/deepbook/sources/state/governance.move:费用参数提案、投票、epoch 更新。
  • packages/deepbook/sources/state/trade_params.movetaker_feemaker_feestake_required 和用户折扣规则。
  • packages/deepbook/sources/vault/deep_price.move:DEEP 与 base/quote 的换算价格,用于 DEEP 支付手续费。
  • packages/deepbook/sources/pool.move:交易、stake、治理、claim rebate、burn DEEP 的对外入口。

小节目录

本章代码

  • book/ch05/code/s01-balance-manager-create/:创建 manager、注册到 registry、生成 proof 和 cap。
  • book/ch05/code/s02-deposit-withdraw/:deposit、withdraw、withdraw_all 的资金路径。
  • book/ch05/code/s03-fee-calculator/:按 trade params、DEEP price、订单方向估算手续费。
  • book/ch05/code/s04-governance-query/:查询治理参数、proposal、TradeParamsUpdateEvent

Move 高阶穿插点

  • BalanceManager 是学习 Move 权限设计的核心案例:owner、trader、cap、proof 分别解决不同信任边界。
  • Vault 展示了协议资产保管的正确姿势:撮合模块不直接转 coin,结算模块统一处理余额变化。
  • 费用和治理章节要把经济规则落回 Move 类型和事件,避免只停留在产品说明。

常见错误

  • 把钱包余额当作可下单余额。
  • 忘记为 pay_with_deep = true 准备 DEEP 余额和价格点。
  • 在 permissionless settlement 中尝试结算仍有 owed 的账户。
  • 认为 stake 后立即获得折扣;实际要看 active stake、成交量和 epoch。
  • 给机器人同时发放 TradeCapWithdrawCap

本章检查清单

  • 能说明 settledowed 分别由谁支付。
  • 能按成交方向解释 base、quote、DEEP 的流向。
  • 能列出 BalanceManager、Vault、Governance 的关键 abort code。
  • 能区分当前参数和下一 epoch 参数。
  • 能为交易前检查设计余额、版本、proof、价格点四类校验。

进阶练习

  1. 设计一张交易费用结算表,列出 maker/taker、买/卖、DEEP/input fee 四种组合。
  2. 手工推演一次卖单成交后 maker 和 taker 的 settled_balancesowed_balances
  3. 写一个监控脚本,订阅 BalanceEventOrderFilledTradeParamsUpdateEvent,重建某个 manager 的资金流水。

ch05-01 为什么使用 BalanceManager

返回本章

先沿资金问问题

这一节把“为什么使用 BalanceManager”放到余额流里看。只看钱包余额会误判交易可用性,真正要追的是权限 proof、托管余额、费用参数和结算路径。

源码入口

沿资金流看

DeepBook 不直接在撮合时从钱包对象扣款。链上订单可能跨交易存在,撮合发生时 maker 不一定参与当前交易,所以协议需要一个池子可验证的托管账户。BalanceManager 正是这个账户,它持有一组按资产类型索引的 Balance<T>,撮合时由 TradeProof 授权池子扣入或转出。

balance_manager.moveBalanceManager 包含 ownerbalancesallow_listedbalances 是用户在 DeepBook 内的可用资产;allow_listed 保存被授权的 TradeCapDepositCapWithdrawCap ID。交易应用应把钱包余额和 BalanceManager 余额分开展示:钱包余额表示还未托管的 Coin<T>BalanceManager.balance<T>() 才是可用于下单的余额。

开发时不要在提交订单前只检查钱包余额。pool.place_limit_orderpool.place_market_order 读取的是 BalanceManager,如果资产没有 deposit 到 manager,后续 vault.settle_balance_manager 会在 withdraw_with_proof 处因余额不足中止。

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

余额类错误最常见于把钱包余额、manager 可用余额、open order 锁定量和 owed balance 混为一谈。交易前检查应分别展示这些数值,并说明哪一项会被本次 PTB 消耗。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、授权 proof、pool version 和费用参数。
  • 钱包余额只表示未托管资产;下单可用余额必须来自 balance_manager.move 的 manager 余额。
  • 涉及费用或返佣时,分别记录 maker/taker、base/quote/DEEP 和当前 epoch 参数。

动手检查

  • 为什么使用 BalanceManager 中哪一步会读写 BalanceManager,哪一步会进入 Vault
  • 失败时应优先排查余额不足、cap/proof 权限、pool pause/version 还是治理参数未生效?
  • 应用侧需要向用户展示 wallet balance、manager balance、locked balance 中的哪几项?

ch05-02 创建、owner 与 TradeProof

返回本章

先沿资金问问题

先沿资金问问题。“创建、owner 与 TradeProof”不是账面说明,而是在回答资产从钱包进入 BalanceManager、被订单锁定、成交后进入 settled/owed,最后由 Vault 收尾的哪一段。

源码入口

关键定义

BalanceManager 本体和交易证明:

public struct BalanceManager has key, store {
    id: UID,
    owner: address,
    balances: Bag,
    allow_listed: VecSet<ID>,
}

public struct TradeProof has drop {
    balance_manager_id: ID,
    trader: address,
}

TradeProof 的生成入口:

public fun generate_proof_as_owner(
    balance_manager: &BalanceManager,
    ctx: &TxContext,
): TradeProof

public fun generate_proof_as_trader(
    balance_manager: &BalanceManager,
    trade_cap: &TradeCap,
    ctx: &TxContext,
): TradeProof

owner 可以直接生成 proof。非 owner 必须持有仍在 allow_listed 里的 TradeCap。这就是 DeepBook 权限模型的核心:交易权限和提款权限可以分开,机器人可以交易但不应能提现。

沿资金流看

balance_manager::new(ctx) 创建 owner 为 ctx.sender() 的 manager,并发出 BalanceManagerEvent { balance_manager_id, owner }new_with_custom_owner(owner, ctx) 允许为指定地址创建。应用若要由托管服务或机器人代操作,应使用 cap 模型:mint_trade_capmint_deposit_capmint_withdraw_cap 只能由 owner 调用,内部会把 cap ID 放入 allow list。

交易需要 TradeProof。owner 可调用 generate_proof_as_owner(balance_manager, ctx);被授权交易者调用 generate_proof_as_trader(balance_manager, trade_cap, ctx)TradeProof 只包含 balance_manager_idtrader,池子在结算时调用 validate_proof,校验 proof 指向当前 manager。错误码包括 EInvalidOwner = 0EInvalidTrader = 1EInvalidProof = 2ECapNotInList = 5

Move 技巧TradeProof 不是“登录态”,而是交易内可验证的权限证据。它把“谁签名”和“谁有资格代表这个 BalanceManager 交易”拆开,适合机器人、托管服务和多交易员系统。

生产应用应把 owner 操作和 trader 操作分离。owner 适合充值、提现和权限管理;做市机器人只应持有 TradeCap,不应持有 WithdrawCap

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

权限路径要区分 owner、TradeCapWithdrawCapDepositCap。应用给机器人做授权时通常只需要交易能力,不应同时授予提款能力;否则撮合之外的资金提取风险会扩大。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、授权 proof、pool version 和费用参数。
  • 钱包余额只表示未托管资产;下单可用余额必须来自 balance_manager.move 的 manager 余额。
  • 涉及费用或返佣时,分别记录 maker/taker、base/quote/DEEP 和当前 epoch 参数。

动手检查

  • 创建、owner 与 TradeProof 中哪一步会读写 BalanceManager,哪一步会进入 Vault
  • 失败时应优先排查余额不足、cap/proof 权限、pool pause/version 还是治理参数未生效?
  • 应用侧需要向用户展示 wallet balance、manager balance、locked balance 中的哪几项?

ch05-03 存入、取出、锁定与结算

返回本章

先沿资金问问题

这一节把“存入、取出、锁定与结算”放到余额流里看。只看钱包余额会误判交易可用性,真正要追的是权限 proof、托管余额、费用参数和结算路径。

源码入口

关键定义

存入和取出入口很直接:

public fun deposit<T>(
    balance_manager: &mut BalanceManager,
    coin: Coin<T>,
    ctx: &mut TxContext,
)

public fun withdraw<T>(
    balance_manager: &mut BalanceManager,
    withdraw_amount: u64,
    ctx: &mut TxContext,
): Coin<T>

public fun withdraw_all<T>(
    balance_manager: &mut BalanceManager,
    ctx: &mut TxContext,
): Coin<T>

每次余额变化都会发出事件:

public struct BalanceEvent has copy, drop {
    balance_manager_id: ID,
    asset: TypeName,
    amount: u64,
    deposit: bool,
}

这说明前端余额页不能只查当前对象字段。要做流水、审计或用户历史,就必须读 BalanceEventdeposit: true 是入账,deposit: false 是出账;asset 是完整类型名,不是 UI ticker。

沿资金流看

存入路径是 balance_manager::deposit<T>(manager, coin, ctx)deposit_with_cap<T>。函数先发出 BalanceEvent { balance_manager_id, asset, amount, deposit: true },再把 Coin<T> 转为 Balance<T>,通过 deposit_with_proof 合并到 balances

取出路径是 withdraw<T>withdraw_all<T>withdraw_with_cap<T>。底层 withdraw_with_proof<T> 会按 BalanceKey<T> 取余额;如果余额小于提现量,抛出 EBalanceManagerBalanceTooLow = 3。提现成功后发出 BalanceEvent { deposit: false }

下单时的锁定不是直接在 BalanceManager 内创建锁字段,而是在 pool.place_order_int 中经过 state.process_create 计算 settledowedowed 表示用户要付给 vault 的资产,settled 表示 vault 要付回用户的资产。对于挂在订单簿中的 maker 订单,订单对象记录了锁定数量;后续成交或取消时再把差额写入 account 的 settled balances。

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

余额类错误最常见于把钱包余额、manager 可用余额、open order 锁定量和 owed balance 混为一谈。交易前检查应分别展示这些数值,并说明哪一项会被本次 PTB 消耗。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、授权 proof、pool version 和费用参数。
  • 钱包余额只表示未托管资产;下单可用余额必须来自 balance_manager.move 的 manager 余额。
  • 涉及费用或返佣时,分别记录 maker/taker、base/quote/DEEP 和当前 epoch 参数。

动手检查

  • 存入、取出、锁定与结算 中哪一步会读写 BalanceManager,哪一步会进入 Vault
  • 失败时应优先排查余额不足、cap/proof 权限、pool pause/version 还是治理参数未生效?
  • 应用侧需要向用户展示 wallet balance、manager balance、locked balance 中的哪几项?

ch05-04 Vault 如何结算 balances_in 与 balances_out

返回本章

先沿资金问问题

先沿资金问问题。“Vault 如何结算 balances_in 与 balances_out”不是账面说明,而是在回答资产从钱包进入 BalanceManager、被订单锁定、成交后进入 settled/owed,最后由 Vault 收尾的哪一段。

源码入口

关键定义

Vault 是池子的真实资产保管层:

public struct Vault<phantom BaseAsset, phantom QuoteAsset> has store {
    base_balance: Balance<BaseAsset>,
    quote_balance: Balance<QuoteAsset>,
    deep_balance: Balance<DEEP>,
}

这三个字段分别保存 base、quote 和 DEEP。撮合不会直接从 maker 钱包转给 taker 钱包,而是先计算 balances_inbalances_out,再由 Vault 与 BalanceManager 做差额结算。

结算时可以把逻辑理解成一个净额表:

if balances_out.asset > balances_in.asset:
    Vault split 差额 -> deposit 到 BalanceManager

if balances_in.asset > balances_out.asset:
    BalanceManager withdraw 差额 -> join 到 Vault

这样写的好处是一次交易可以同时处理成交、费用、返还和剩余锁定释放,而不是在撮合循环中到处转 coin。

沿资金流看

Vault<BaseAsset, QuoteAsset> 持有 base_balancequote_balancedeep_balance。它是池子的真实资产金库。pool.place_order_int 的资金路径是:

  1. 生成 OrderInfo,调用 book.create_order 撮合或入簿。
  2. state.process_create 更新 account,返回 (settled, owed)
  3. vault.settle_balance_manager(settled, owed, balance_manager, trade_proof) 执行资产划转。
  4. OrderInfo 发出订单和成交事件。

vault.settle_balance_manager 中,如果 balances_out.base() > balances_in.base(),vault 从 base_balance split 差额并 deposit 到 manager;如果 balances_in.base() > balances_out.base(),manager 通过 withdraw_with_proof 扣出差额并 join 到 vault。quote 和 DEEP 同理。

withdraw_settled_amounts_permissionless 只能处理无 owed 的场景。settle_balance_manager_permissionless 要求 balances_in 三项都是 0,否则 EHasOwedBalances = 8;同时必须存在可领取余额,否则 ENoBalanceToSettle = 7

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

余额类错误最常见于把钱包余额、manager 可用余额、open order 锁定量和 owed balance 混为一谈。交易前检查应分别展示这些数值,并说明哪一项会被本次 PTB 消耗。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、授权 proof、pool version 和费用参数。
  • 钱包余额只表示未托管资产;下单可用余额必须来自 balance_manager.move 的 manager 余额。
  • 涉及费用或返佣时,分别记录 maker/taker、base/quote/DEEP 和当前 epoch 参数。

动手检查

  • Vault 如何结算 balances_in 与 balances_out 中哪一步会读写 BalanceManager,哪一步会进入 Vault
  • 失败时应优先排查余额不足、cap/proof 权限、pool pause/version 还是治理参数未生效?
  • 应用侧需要向用户展示 wallet balance、manager balance、locked balance 中的哪几项?

ch05-05 maker fee、taker fee、protocol fee 与 rebate

返回本章

先沿资金问问题

这一节把“maker fee、taker fee、protocol fee 与 rebate”放到余额流里看。只看钱包余额会误判交易可用性,真正要追的是权限 proof、托管余额、费用参数和结算路径。

源码入口

沿资金流看

trade_params.move 只保存核心交易费率:taker_feemaker_feestake_requiredOrderInfo 记录成交过程中的 paid_feesmaker_feesOrderFilled 事件会输出 taker_feetaker_fee_is_deepmaker_feemaker_fee_is_deep

taker fee 是主动吃单的一方支付的费用。pool.get_quantity_outget_quantity_in 会用 governance.trade_params().taker_fee() 做交易前估算。maker fee 是挂单成交时对应的 maker 费用或返佣参数,历史 maker fee 会进入 state.history,取消订单时 process_cancel 根据订单 epoch 的 maker fee 计算退款。

DeepBook 的手续费资产可以是 DEEP,也可以是输入资产。pay_with_deep = true 时,deep_price.get_order_deep_price 给出 OrderDeepPricedeep_price::fee_quantity 把成交规模换算成 DEEP 数量;输入资产付费时,买单从 quote 侧加费,卖单从 base 侧加费,并应用 constants::fee_penalty_multiplier()

本章把 protocol fee 理解为流入协议 vault 并后续可治理处理的费用。pool.burn_deep 会从历史累计的待销毁 DEEP 中调用 vault.withdraw_deep_to_burn,再用 token::deep::burn 销毁,并发出 DeepBurned { pool_id, deep_burned }

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

费用阅读顺序建议从 packages/deepbook/sources/state/trade_params.move 的当前参数开始,再看 packages/deepbook/sources/state/account.move 中 active stake 和 rebate 记录。pay_with_deep 会把手续费支付资产切到 DEEP,因此交易前检查必须同时覆盖 base/quote 余额、DEEP 余额和 deep price 是否可用。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、授权 proof、pool version 和费用参数。
  • 钱包余额只表示未托管资产;下单可用余额必须来自 balance_manager.move 的 manager 余额。
  • 涉及费用或返佣时,分别记录 maker/taker、base/quote/DEEP 和当前 epoch 参数。

动手检查

  • maker fee、taker fee、protocol fee 与 rebate 中哪一步会读写 BalanceManager,哪一步会进入 Vault
  • 失败时应优先排查余额不足、cap/proof 权限、pool pause/version 还是治理参数未生效?
  • 应用侧需要向用户展示 wallet balance、manager balance、locked balance 中的哪几项?

ch05-06 DEEP staking 如何影响费用等级

返回本章

先沿资金问问题

先沿资金问问题。“DEEP staking 如何影响费用等级”不是账面说明,而是在回答资产从钱包进入 BalanceManager、被订单锁定、成交后进入 settled/owed,最后由 Vault 收尾的哪一段。

源码入口

沿资金流看

pool.stake 要求 amount > 0,否则 EInvalidStake = 13。它调用 state.process_stake,账户侧 account.add_stake 会把 stake 加入 inactive_stake,并把同等 DEEP 加入 owed_balances,随后 vault 从 BalanceManager 扣 DEEP。

account.update 在新 epoch 把 inactive_stake 转入 active_staketrade_params::taker_fee_for_user(active_stake, volume_in_deep) 规定:当用户 active stake 大于等于 stake_required,且按 DEEP 计价的交易量也大于等于 stake_required,taker fee 减半。否则使用完整 taker fee。

pool.unstake 会调用 state.process_unstake,账户的 remove_stake 把 active 和 inactive stake 清零,并把 DEEP 加回 settled_balances。应用侧应提示 unstake 会清除投票状态,并且 staking 折扣不是当前交易立即生效,而依赖 epoch 刷新和历史交易量。

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

费用阅读顺序建议从 packages/deepbook/sources/state/trade_params.move 的当前参数开始,再看 packages/deepbook/sources/state/account.move 中 active stake 和 rebate 记录。pay_with_deep 会把手续费支付资产切到 DEEP,因此交易前检查必须同时覆盖 base/quote 余额、DEEP 余额和 deep price 是否可用。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、授权 proof、pool version 和费用参数。
  • 钱包余额只表示未托管资产;下单可用余额必须来自 balance_manager.move 的 manager 余额。
  • 涉及费用或返佣时,分别记录 maker/taker、base/quote/DEEP 和当前 epoch 参数。

动手检查

  • DEEP staking 如何影响费用等级 中哪一步会读写 BalanceManager,哪一步会进入 Vault
  • 失败时应优先排查余额不足、cap/proof 权限、pool pause/version 还是治理参数未生效?
  • 应用侧需要向用户展示 wallet balance、manager balance、locked balance 中的哪几项?

ch05-07 trade_params 中的交易参数

返回本章

先沿资金问问题

费用参数看起来只是几个数字,但它们会影响交易成本、DEEP staking 折扣、治理投票和前端报价。读这一节时不要只问 taker fee 是多少,要问这组参数在哪个 epoch 生效、对哪个账户生效、是否还能被治理改掉。

这也是应用很容易出错的地方:用户钱包有钱,不代表 BalanceManager 里有可用余额;用户 stake 足够,也不代表本次交易一定拿到折扣。

源码入口

按这个顺序读会比较稳:

关键定义

TradeParams 只保存三项参数,但它们会影响交易费、返佣展示、stake 门槛和治理投票权的解释。

public struct TradeParams has copy, drop, store {
    taker_fee: u64,
    maker_fee: u64,
    stake_required: u64,
}

public(package) fun taker_fee_for_user(
    self: &TradeParams,
    active_stake: u64,
    volume_in_deep: u128,
): u64 {
    if (
        active_stake >= self.stake_required &&
        volume_in_deep >= (self.stake_required as u128)
    ) {
        self.taker_fee / 2
    } else {
        self.taker_fee
    }
}

这段折扣逻辑同时看 active_stake 和以 DEEP 计价的交易量。应用如果只展示“已 stake 足够”会误导用户;还必须检查最近成交量是否达到门槛,并且所有 fee 计算都要使用链上整数精度。

沿资金流看

TradeParams 的字段不多,但每个字段都跨了产品和协议两层:

  • taker_fee:主动成交方费率,按 constants::float_scaling() 精度参与计算。
  • maker_fee:被动挂单方费用或 rebate 相关参数。
  • stake_required:获得 taker fee 折扣和参与治理的关键门槛。

pool.pool_trade_params() 返回当前参数,pool.pool_next_trade_params() 返回下一 epoch 参数。这个区分对前端很实际:如果治理投票刚通过,交易预览应该告诉用户“当前交易仍按当前参数计算”,不要提前展示下一 epoch 的费率。

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

费用阅读顺序建议从 packages/deepbook/sources/state/trade_params.move 的当前参数开始,再看 packages/deepbook/sources/state/account.move 中 active stake 和 rebate 记录。pay_with_deep 会把手续费支付资产切到 DEEP,因此交易前检查必须同时覆盖 base/quote 余额、DEEP 余额和 deep price 是否可用。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、proof、pool version、当前 fee 参数和 DEEP 余额。
  • 计算折扣时必须同时看 active stake 和 DEEP 计价 volume;只满足一项不会减半。
  • 历史成交展示要保存当时的 epoch 参数,否则事后用当前参数重算会对不上链上事件。

动手检查

  • 一个用户 stake 已达标但 volume 未达标,taker_fee_for_user 会返回什么?
  • 治理更新了 next params 后,当前 epoch 的交易预览应该显示哪组参数?
  • 如果用户选择用 DEEP 支付手续费,交易前要多检查哪一种余额?

ch05-08 deep_price 如何服务 DEEP 价格计算

返回本章

先沿资金问问题

先沿资金问问题。“deep_price 如何服务 DEEP 价格计算”不是账面说明,而是在回答资产从钱包进入 BalanceManager、被订单锁定、成交后进入 settled/owed,最后由 Vault 收尾的哪一段。

源码入口

沿资金流看

deep_price.moveDeepPrice 分别维护 base 和 quote 的价格点以及累计值。add_price_point 要求同一方向价格点至少间隔一分钟,否则 EDataPointRecentlyAdded = 1;如果没有任何价格点,calculate_order_deep_price 抛出 ENoDataPoints = 2

pool.add_deep_price_point 会用参考池和目标池更新 DEEP 价格,并发出 PriceAdded { conversion_rate, timestamp, is_base_conversion, reference_pool, target_pool }。交易时 get_order_deep_price(whitelisted) 会对 whitelisted pool 返回 deep_per_asset = 0,表示免手续费;普通池使用最近一天内价格点平均值。

应用下单前如果允许 pay_with_deep = true,必须先 dry run get_quantity_outget_quote_quantity_in,确认需要的 DEEP 数量,并检查 manager 的 DEEP 余额加 settled DEEP 是否足够。

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

费用阅读顺序建议从 packages/deepbook/sources/state/trade_params.move 的当前参数开始,再看 packages/deepbook/sources/state/account.move 中 active stake 和 rebate 记录。pay_with_deep 会把手续费支付资产切到 DEEP,因此交易前检查必须同时覆盖 base/quote 余额、DEEP 余额和 deep price 是否可用。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、授权 proof、pool version 和费用参数。
  • 钱包余额只表示未托管资产;下单可用余额必须来自 balance_manager.move 的 manager 余额。
  • 涉及费用或返佣时,分别记录 maker/taker、base/quote/DEEP 和当前 epoch 参数。

动手检查

  • deep_price 如何服务 DEEP 价格计算 中哪一步会读写 BalanceManager,哪一步会进入 Vault
  • 失败时应优先排查余额不足、cap/proof 权限、pool pause/version 还是治理参数未生效?
  • 应用侧需要向用户展示 wallet balance、manager balance、locked balance 中的哪几项?

ch05-09 治理提案、投票与参数更新

返回本章

先沿资金问问题

这一节把“治理提案、投票与参数更新”放到余额流里看。只看钱包余额会误判交易可用性,真正要追的是权限 proof、托管余额、费用参数和结算路径。

源码入口

关键定义

治理状态把“本 epoch 的提案集合”和“当前/下一 epoch 参数”放在一起。读参数更新时,要区分已经生效的 trade_params 和等待 epoch 切换的 next_trade_params

public struct Proposal has copy, drop, store {
    taker_fee: u64,
    maker_fee: u64,
    stake_required: u64,
    votes: u64,
}

public struct Governance has store {
    epoch: u64,
    whitelisted: bool,
    stable: bool,
    proposals: VecMap<ID, Proposal>,
    trade_params: TradeParams,
    next_trade_params: TradeParams,
    voting_power: u64,
    quorum: u64,
}

public struct TradeParamsUpdateEvent has copy, drop {
    taker_fee: u64,
    maker_fee: u64,
    stake_required: u64,
}

事件只告诉你“新参数是什么”,不告诉你每个账户为什么这样投。Indexer 如果要构建治理面板,需要同时保存 proposal、vote 调整、stake 变化和 epoch 更新,否则只能展示最终参数,无法解释投票过程。

沿资金流看

pool.submit_proposal 接收 taker_feemaker_feestake_required,要求 BalanceManager 有有效 proof,并由 state.process_proposal 检查账户 stake。governance.add_proposal 校验 fee 是否为 FEE_MULTIPLE = 1000 的倍数,并根据 stable 或 volatile 池限制上下界。whitelisted pool 不能改参数,错误码是 EWhitelistedPoolCannotChange = 5

pool.vote 使用账户全部 voting power 投票。governance.adjust_vote 会把旧 proposal 的票移除,并给新 proposal 加票;当 proposal votes 超过 quorumnext_trade_params 更新为该 proposal。governance.update 在新 epoch 清空 proposals,把 next_trade_params 变成当前 trade_params,并发出 TradeParamsUpdateEvent { taker_fee, maker_fee, stake_required }

投票权来自 stake_to_voting_power。100k DEEP 以下按 stake 线性计算,超过阈值后只增加平方根部分,避免单个巨鲸线性垄断。

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

治理和维护能力影响的是参数生效、暂停和版本门禁,不应被前端当作普通余额错误处理。PTB dry run 返回这些 abort 时,提示语应引导用户等待版本升级或市场恢复,而不是要求用户继续充值。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、授权 proof、pool version 和费用参数。
  • 钱包余额只表示未托管资产;下单可用余额必须来自 balance_manager.move 的 manager 余额。
  • 涉及费用或返佣时,分别记录 maker/taker、base/quote/DEEP 和当前 epoch 参数。

动手检查

  • 治理提案、投票与参数更新 中哪一步会读写 BalanceManager,哪一步会进入 Vault
  • 失败时应优先排查余额不足、cap/proof 权限、pool pause/version 还是治理参数未生效?
  • 应用侧需要向用户展示 wallet balance、manager balance、locked balance 中的哪几项?

ch05-10 pause、version 与 maintainer 能力边界

返回本章

先沿资金问问题

先沿资金问问题。“pause、version 与 maintainer 能力边界”不是账面说明,而是在回答资产从钱包进入 BalanceManager、被订单锁定、成交后进入 settled/owed,最后由 Vault 收尾的哪一段。

源码入口

沿资金流看

DeepBookV3 的版本边界主要在 registry.movepool.moveRegistry 维护全局 enabled versions,enable_versiondisable_version 只能由 DeepbookAdminCap 调用;禁用当前版本会触发 ECannotDisableCurrentVersion = 6。Pool 内部也有 allowed_versionsupdate_allowed_versions 是 admin 入口。

创建池和注册池有两条路径:create_permissionless_pool 要支付固定 DEEP creation fee,并在 registry 注册;create_pool_admin 由 admin 创建,可指定 whitelisted 或 stable 状态。PoolCreated 事件记录 pool_idtaker_feemaker_feetick_sizelot_sizemin_sizewhitelisted_pooltreasury_address

应用侧不要假设一个池永远可交易。交易前应检查 registry 是否启用版本、pool 是否 registered、以及池对象类型是否与 base/quote 匹配。

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

治理和维护能力影响的是参数生效、暂停和版本门禁,不应被前端当作普通余额错误处理。PTB dry run 返回这些 abort 时,提示语应引导用户等待版本升级或市场恢复,而不是要求用户继续充值。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、授权 proof、pool version 和费用参数。
  • 钱包余额只表示未托管资产;下单可用余额必须来自 balance_manager.move 的 manager 余额。
  • 涉及费用或返佣时,分别记录 maker/taker、base/quote/DEEP 和当前 epoch 参数。

动手检查

  • pause、version 与 maintainer 能力边界 中哪一步会读写 BalanceManager,哪一步会进入 Vault
  • 失败时应优先排查余额不足、cap/proof 权限、pool pause/version 还是治理参数未生效?
  • 应用侧需要向用户展示 wallet balance、manager balance、locked balance 中的哪几项?

ch05-11 资金安全相关 abort code

返回本章

先沿资金问问题

这一节把“资金安全相关 abort code”放到余额流里看。只看钱包余额会误判交易可用性,真正要追的是权限 proof、托管余额、费用参数和结算路径。

源码入口

沿资金流看

重点错误码如下:

  • balance_manager.moveEInvalidOwner = 0EInvalidTrader = 1EInvalidProof = 2EBalanceManagerBalanceTooLow = 3EMaxCapsReached = 4ECapNotInList = 5
  • vault.moveENoBalanceToSettle = 7EHasOwedBalances = 8 与 permissionless settlement 相关。
  • pool.moveEInvalidQuantityIn = 6EInvalidOrderBalanceManager = 9EMinimumQuantityOutNotMet = 12EInvalidStake = 13EPoolNotRegistered = 14EWrongPoolReferral = 20
  • governance.moveEInvalidMakerFee = 1EInvalidTakerFee = 2EProposalDoesNotExist = 3EMaxProposalsReachedNotEnoughVotes = 4EWhitelistedPoolCannotChange = 5

前端错误提示应把 Move abort 映射成可操作信息:余额不足提示充值到 BalanceManager;proof 错误提示重新选择 manager 或授权 cap;版本错误提示刷新池列表;治理 fee 错误提示检查 stable/volatile 上下界和 1000 倍数。

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

余额类错误最常见于把钱包余额、manager 可用余额、open order 锁定量和 owed balance 混为一谈。交易前检查应分别展示这些数值,并说明哪一项会被本次 PTB 消耗。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、授权 proof、pool version 和费用参数。
  • 钱包余额只表示未托管资产;下单可用余额必须来自 balance_manager.move 的 manager 余额。
  • 涉及费用或返佣时,分别记录 maker/taker、base/quote/DEEP 和当前 epoch 参数。

动手检查

  • 资金安全相关 abort code 中哪一步会读写 BalanceManager,哪一步会进入 Vault
  • 失败时应优先排查余额不足、cap/proof 权限、pool pause/version 还是治理参数未生效?
  • 应用侧需要向用户展示 wallet balance、manager balance、locked balance 中的哪几项?

ch05-12 余额不一致、权限错误、版本错误

返回本章

先沿资金问问题

先沿资金问问题。“余额不一致、权限错误、版本错误”不是账面说明,而是在回答资产从钱包进入 BalanceManager、被订单锁定、成交后进入 settled/owed,最后由 Vault 收尾的哪一段。

源码入口

沿资金流看

余额不一致通常来自三类问题。第一,用户只看钱包余额,没有把资产 deposit 到 BalanceManager。第二,用户有 settled_balances 但没有调用 withdraw_settled_amounts 把它们落回 manager。第三,使用输入资产付费时没有额外预留 fee,导致下单模拟成功数量和实际可扣数量不同。

权限错误多发生在托管机器人。TradeCap 只能生成交易 proof,不能提现;WithdrawCap 可以从 manager 提现,应严格隔离。撤销 cap 后,继续用旧 cap 会在 allow list 校验失败。

版本错误来自 registry 或 pool allowed versions。应用缓存池列表时要带版本字段,提交 PTB 前做一次轻量刷新或 dry run。

资金旁白:这条线不要从撮合引擎开始。钱包里的 Coin<T> 先进入 balance_manager.move 的托管余额,成交后的 settled/owed 记录在 account.move,真正资产进出由 vault.move 收尾。读费用和余额时,始终沿这条资金线核对。

权限路径要区分 owner、TradeCapWithdrawCapDepositCap。应用给机器人做授权时通常只需要交易能力,不应同时授予提款能力;否则撮合之外的资金提取风险会扩大。

资金安全判断

  • 交易前同时检查 BalanceManager 余额、授权 proof、pool version 和费用参数。
  • 钱包余额只表示未托管资产;下单可用余额必须来自 balance_manager.move 的 manager 余额。
  • 涉及费用或返佣时,分别记录 maker/taker、base/quote/DEEP 和当前 epoch 参数。

动手检查

  • 余额不一致、权限错误、版本错误 中哪一步会读写 BalanceManager,哪一步会进入 Vault
  • 失败时应优先排查余额不足、cap/proof 权限、pool pause/version 还是治理参数未生效?
  • 应用侧需要向用户展示 wallet balance、manager balance、locked balance 中的哪几项?

ch07 DeepBookV3 闪电贷与组合交易

本章目标

本章只讨论 DeepBookV3 Spot pool 的闪电贷。DeepBookV3 确实存在闪电贷,入口在 packages/deepbook/sources/pool.move,底层实现位于 packages/deepbook/sources/vault/vault.move。它使用 hot potato 模式强制同一交易归还资产,没有跨交易债务状态,不要和 packages/deepbook_margin 的普通借贷混淆。

本章学习阶梯

  • L2 先理解“同一交易借出再归还”的资源闭环。
  • L3 读 FlashLoan hot potato、borrow/return 和 Vault 校验。
  • L4 构造一个成功和一个失败的闪电贷 PTB。
  • L5 能把闪电贷放进套利、清算或组合交易,并设计风险检查。

关键定义卡片

DeepBookV3 闪电贷的定义在 vault.move,入口暴露在 pool.move

public struct FlashLoan {
    pool_id: ID,
    borrow_quantity: u64,
    type_name: TypeName,
}

FlashLoan 没有 drop,所以它是 hot potato。借出后必须在同一笔交易里归还并消费掉。三个字段就是归还校验条件:池子必须相同,资产类型必须相同,数量必须完全相同。

public fun borrow_flashloan_base<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    base_amount: u64,
    ctx: &mut TxContext,
): (Coin<BaseAsset>, FlashLoan)

public fun return_flashloan_base<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    coin: Coin<BaseAsset>,
    flash_loan: FlashLoan,
)

借 quote 也有对应的 quote 版本。不要把这里和 Margin 借款混淆:闪电贷没有跨交易债务 shares,也没有还款计划,失败就整笔交易 abort。

源码地图

  • packages/deepbook/sources/pool.moveborrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.moveFlashLoanFlashLoanBorrowed、底层借出和归还校验。
  • crates/indexer/src/handlers/flash_loan_handler.rs:把 FlashLoanBorrowed 事件写入 flashloans 表。

小节目录

本章代码

  • book/ch07/code/s01-flashloan-move-wrapper/:Move wrapper 调用借出和归还。
  • book/ch07/code/s02-flashloan-ptb/:TypeScript PTB 构造示例。
  • book/ch07/code/s03-flashloan-event-query/:查询 FlashLoanBorrowed 事件。
  • book/ch07/code/s04-flashloan-indexer-row/:展示 flashloans 表记录。

Move 高阶穿插点

  • 闪电贷是 hot potato 模式的高级案例:资源必须在同一交易里被消费,类型系统参与安全保证。
  • 借 base 和借 quote 要分别追踪 type name、pool id 和数量,任何一个维度错都会 abort。
  • 组合交易读法是先保证资源闭环,再谈套利、清算或跨协议收益。

常见错误

  • 把 DeepBookV3 闪电贷写成 Margin 借贷。
  • 以为可以跨交易归还。
  • 多还或少还本金。
  • 借 base 后用 quote return。
  • 只查询 borrow 事件,不检查交易是否成功。

本章检查清单

  • 能指出闪电贷入口在 pool.move,底层在 vault/vault.move
  • 能解释 FlashLoan hot potato 的三个字段。
  • 能写出 base 和 quote 借还路径。
  • 能列出六个闪电贷错误码。
  • 能说明 indexer flashloans 表只有 borrow 方向事件。

进阶练习

  1. 编写一个 PTB,借 base 后调用一次 swap,再归还 base。
  2. 构造错误 pool 归还的 dry run,记录 abort code。
  3. 写 SQL 统计每个 pool 每日闪电贷借出总量和最大单笔数量。

ch07-01 DeepBookV3 闪电贷入口

返回本章

先看交易边界

读这一节时,把“DeepBookV3 闪电贷入口”放进一个 PTB 里推演。重点不是能不能借出资产,而是 hot potato 如何强迫调用方在同一交易中归还。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

pool.move 暴露四个 public 函数:

  • borrow_flashloan_base<BaseAsset, QuoteAsset>(&mut Pool, base_amount, ctx): (Coin<BaseAsset>, FlashLoan)
  • borrow_flashloan_quote<BaseAsset, QuoteAsset>(&mut Pool, quote_amount, ctx): (Coin<QuoteAsset>, FlashLoan)
  • return_flashloan_base<BaseAsset, QuoteAsset>(&mut Pool, Coin<BaseAsset>, FlashLoan)
  • return_flashloan_quote<BaseAsset, QuoteAsset>(&mut Pool, Coin<QuoteAsset>, FlashLoan)

Pool 入口只负责加载 PoolInner,把 pool ID、数量和 coin 转交给 vault。真实资产在 Vaultbase_balancequote_balance 中 split 和 join。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

调用链是 pool.move borrow 入口取出 pool 内部 vault,vault.movebase_balancequote_balance split 出 Coin,同时返回 FlashLoan hot potato。PTB 中间可以组合 swap、套利或清算式动作,但最后必须把同一个方向的本金和 FlashLoan 一起交回 return 入口。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • DeepBookV3 闪电贷入口 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-02 hot potato 为什么强制同一交易归还

返回本章

先看交易边界

这里先把问题放进同一笔交易里。“hot potato 为什么强制同一交易归还”成立的前提是借出、使用、归还都在同一笔交易里完成;任何跨交易债务想象都应该立刻排除。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

FlashLoan 没有 keystoredrop 能力,只是一个必须被消费的普通值。借出函数返回 (Coin<T>, FlashLoan);如果调用者不把 FlashLoan 传给对应 return 函数,交易结束时这个值无法被丢弃或存储,Move 类型系统会让交易失败。

这就是 hot potato 模式:债务凭证不能跨交易保存,只能在同一 PTB 内被还款函数解包。协议不需要记录借款账户、利息累计或清算状态。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

归还校验关注三件事:pool id 是否匹配、借出资产方向是否匹配、归还数量是否等于 FlashLoan 记录的 amount。任意不一致都会让交易整体 abort,之前组合步骤的状态也不会落链。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • hot potato 为什么强制同一交易归还 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-03 FlashLoan 结构

返回本章

先看交易边界

读这一节时,把“FlashLoan 结构”放进一个 PTB 里推演。重点不是能不能借出资产,而是 hot potato 如何强迫调用方在同一交易中归还。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

vault.move 定义:

public struct FlashLoan {
    pool_id: ID,
    borrow_quantity: u64,
    type_name: TypeName,
}

pool_id 绑定借出的池子;borrow_quantity 绑定必须归还的精确数量;type_name 绑定借出的资产类型。归还时三项都要匹配,否则 abort。

Move 技巧FlashLoan 是 hot potato 的典型用法。它没有 drop,也不会作为长期债务对象保存;调用者必须在同一笔交易里把它交给 return 函数消费掉,否则交易无法结束。

注意 FlashLoan 事件只有借出事件 FlashLoanBorrowed,没有单独的 return 事件。成功归还体现在同一交易没有 abort,vault 余额通过 coin join 回来。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

归还校验关注三件事:pool id 是否匹配、借出资产方向是否匹配、归还数量是否等于 FlashLoan 记录的 amount。任意不一致都会让交易整体 abort,之前组合步骤的状态也不会落链。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • FlashLoan 结构 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-04 borrow_flashloan_base 路径

返回本章

先看交易边界

这里先把问题放进同一笔交易里。“borrow_flashloan_base 路径”成立的前提是借出、使用、归还都在同一笔交易里完成;任何跨交易债务想象都应该立刻排除。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

关键定义

Pool 层公开入口:

public fun borrow_flashloan_base<BaseAsset, QuoteAsset>(
    self: &mut Pool<BaseAsset, QuoteAsset>,
    base_amount: u64,
    ctx: &mut TxContext,
): (Coin<BaseAsset>, FlashLoan)

Vault 层底层实现返回两个资源:

public(package) fun borrow_flashloan_base<BaseAsset, QuoteAsset>(
    self: &mut Vault<BaseAsset, QuoteAsset>,
    pool_id: ID,
    borrow_quantity: u64,
    ctx: &mut TxContext,
): (Coin<BaseAsset>, FlashLoan)

返回值 (Coin<BaseAsset>, FlashLoan) 必须一起读。Coin<BaseAsset> 是你借到的资产,FlashLoan 是必须在同一 PTB 中归还时消费的证明。没有 FlashLoan,你无法完成 return;没有 return,交易无法结束。

在一笔交易里走完

调用路径:

  1. 应用在 PTB 中调用 pool::borrow_flashloan_base<BaseAsset, QuoteAsset>(&mut pool, base_amount, ctx)
  2. pool 加载 PoolInner,调用 vault.borrow_flashloan_base(self.pool_id, base_amount, ctx)
  3. vault 校验 borrow_quantity > 0,否则 EInvalidLoanQuantity = 3
  4. vault 校验 base_balance.value() >= borrow_quantity,否则 ENotEnoughBaseForLoan = 1
  5. vault 从 base_balance split 指定数量,转成 Coin<BaseAsset>
  6. vault 构造 FlashLoan { pool_id, borrow_quantity, type_name: BaseAsset }
  7. 发出 FlashLoanBorrowed { pool_id, borrow_quantity, type_name }

拿到的 Coin<BaseAsset> 可以继续在同一 PTB 中用于 swap、套利、再平衡或其它组合操作。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

调用链是 pool.move borrow 入口取出 pool 内部 vault,vault.movebase_balancequote_balance split 出 Coin,同时返回 FlashLoan hot potato。PTB 中间可以组合 swap、套利或清算式动作,但最后必须把同一个方向的本金和 FlashLoan 一起交回 return 入口。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • borrow_flashloan_base 路径 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-05 borrow_flashloan_quote 路径

返回本章

先看交易边界

读这一节时,把“borrow_flashloan_quote 路径”放进一个 PTB 里推演。重点不是能不能借出资产,而是 hot potato 如何强迫调用方在同一交易中归还。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

quote 借出与 base 对称:

  1. 调用 pool::borrow_flashloan_quote<BaseAsset, QuoteAsset>(&mut pool, quote_amount, ctx)
  2. pool 转发给 vault.borrow_flashloan_quote
  3. 数量为 0 触发 EInvalidLoanQuantity = 3
  4. quote vault 余额不足触发 ENotEnoughQuoteForLoan = 2
  5. 返回 Coin<QuoteAsset> 和记录 quote type name 的 FlashLoan
  6. 发出 FlashLoanBorrowed

应用应根据套利路径决定借 base 还是借 quote。借错方向会导致后续还款类型不匹配,无法通过 return 校验。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

调用链是 pool.move borrow 入口取出 pool 内部 vault,vault.movebase_balancequote_balance split 出 Coin,同时返回 FlashLoan hot potato。PTB 中间可以组合 swap、套利或清算式动作,但最后必须把同一个方向的本金和 FlashLoan 一起交回 return 入口。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • borrow_flashloan_quote 路径 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-06 return_flashloan_base 校验

返回本章

先看交易边界

这里先把问题放进同一笔交易里。“return_flashloan_base 校验”成立的前提是借出、使用、归还都在同一笔交易里完成;任何跨交易债务想象都应该立刻排除。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

vault.return_flashloan_base 的校验顺序:

  1. pool_id == flash_loan.pool_id,否则 EIncorrectLoanPool = 4
  2. type_name::with_defining_ids<BaseAsset>() == flash_loan.type_name,否则 EIncorrectTypeReturned = 5
  3. coin.value() == flash_loan.borrow_quantity,否则 EIncorrectQuantityReturned = 6
  4. self.base_balance.join(coin.into_balance<BaseAsset>()) 把资产归还 vault。
  5. 解构 FlashLoan,消费 hot potato。

归还数量必须精确等于借出数量。多还或少还都失败。如果组合交易产生了利润,应先 split 出利润,只把本金数量传给 return 函数。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

归还校验关注三件事:pool id 是否匹配、借出资产方向是否匹配、归还数量是否等于 FlashLoan 记录的 amount。任意不一致都会让交易整体 abort,之前组合步骤的状态也不会落链。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • return_flashloan_base 校验 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-07 return_flashloan_quote 校验

返回本章

先看交易边界

读这一节时,把“return_flashloan_quote 校验”放进一个 PTB 里推演。重点不是能不能借出资产,而是 hot potato 如何强迫调用方在同一交易中归还。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

return_flashloan_quote 与 base 对称,只是校验的 type name 是 QuoteAsset,归还到 quote_balance。常见失败是把借出的 quote 在其它模块换成 base 后直接尝试归还 base,或者从另一个 pool 调用 return。两者分别触发类型错误或池子错误。

PTB 中建议把借出 coin、业务操作、归还 coin 和利润 coin 明确命名,避免把归还本金和利润对象混在一起。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

归还校验关注三件事:pool id 是否匹配、借出资产方向是否匹配、归还数量是否等于 FlashLoan 记录的 amount。任意不一致都会让交易整体 abort,之前组合步骤的状态也不会落链。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • return_flashloan_quote 校验 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-08 错误码

返回本章

先看交易边界

这里先把问题放进同一笔交易里。“错误码”成立的前提是借出、使用、归还都在同一笔交易里完成;任何跨交易债务想象都应该立刻排除。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

vault.move 闪电贷相关错误码:

  • ENotEnoughBaseForLoan = 1:base vault 余额不足。
  • ENotEnoughQuoteForLoan = 2:quote vault 余额不足。
  • EInvalidLoanQuantity = 3:借款数量为 0。
  • EIncorrectLoanPool = 4:归还到错误 pool。
  • EIncorrectTypeReturned = 5:归还资产类型和借出类型不同。
  • EIncorrectQuantityReturned = 6:归还数量不等于借出数量。

这些错误都不是 Margin 借贷错误。它们没有债务账户、利率、抵押率或清算逻辑。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

归还校验关注三件事:pool id 是否匹配、借出资产方向是否匹配、归还数量是否等于 FlashLoan 记录的 amount。任意不一致都会让交易整体 abort,之前组合步骤的状态也不会落链。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • 错误码 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-09 FlashLoanBorrowed 事件与 flashloans 表

返回本章

先看交易边界

读这一节时,把“FlashLoanBorrowed 事件与 flashloans 表”放进一个 PTB 里推演。重点不是能不能借出资产,而是 hot potato 如何强迫调用方在同一交易中归还。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • crates/indexer/src/handlers/flash_loan_handler.rs:把 FlashLoanBorrowed 事件写入 flashloans 表,注意它只记录借出事件。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

vault.borrow_flashloan_baseborrow_flashloan_quote 都发出:

public struct FlashLoanBorrowed has copy, drop {
    pool_id: ID,
    borrow_quantity: u64,
    type_name: TypeName,
}

Indexer 处理器在 crates/indexer/src/handlers/flash_loan_handler.rs 中把事件写入 flashloans 表,字段包括 event_digestdigestsendercheckpointcheckpoint_timestamp_mspackagepool_idborrow_quantityborrow = truetype_name

因为没有 return 事件,分析闪电贷成功与否应以包含该事件的交易最终成功为前提。失败交易不会产生最终可索引事件。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

FlashLoanBorrowed 是借出方向事件,indexer 落库后只能证明成功交易里发生过 borrow。归还没有独立表行;如果交易最终失败,事件和表记录也不会成为成功状态的一部分。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • FlashLoanBorrowed 事件与 flashloans 表 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-10 与 Margin 借贷的差异

返回本章

先看交易边界

这里先把问题放进同一笔交易里。“与 Margin 借贷的差异”成立的前提是借出、使用、归还都在同一笔交易里完成;任何跨交易债务想象都应该立刻排除。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

DeepBookV3 闪电贷:

  • 入口在 packages/deepbook/sources/pool.move
  • 资金来自 Spot pool 的 Vault
  • 借出和归还必须在同一 PTB。
  • FlashLoan hot potato 强制归还。
  • 没有利率、抵押物、健康因子、清算和跨交易债务。

DeepBook Margin 普通借贷:

  • 源码在 packages/deepbook_margin
  • 有账户、抵押、借款、还款、清算等长期状态。
  • 债务可以跨交易存在。

写文档、代码和事件解析时必须使用 DeepBookV3 的 FlashLoanBorrowed,不要引用 Margin 的 borrow/repay 事件替代。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

hot potato 的安全性来自 Move 资源必须被消费的类型约束。开发 wrapper 时不要试图把 FlashLoan 存进对象或跨交易传递,正确做法是在同一个 PTB 中 borrow、使用资产、return。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • 与 Margin 借贷的差异 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-11 组合交易场景

返回本章

先看交易边界

读这一节时,把“组合交易场景”放进一个 PTB 里推演。重点不是能不能借出资产,而是 hot potato 如何强迫调用方在同一交易中归还。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

闪电贷适合在单笔 PTB 内完成可原子验证的策略:

  • 跨池套利:从 A 池借 base,在 B 池换成 quote,再换回 base,归还本金,留下利润。
  • 库存再平衡:临时借 quote 买回 base,使多个池子的做市库存回到目标比例。
  • 批量清算辅助:借入临时资产完成外部协议操作,再用获得的资产归还。
  • 迁移流动性:短暂补足某个资产缺口,完成多对象资金搬迁。

所有场景都必须在 PTB 内保证最终有精确本金数量的 Coin<T> 传给 return 函数。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

调用链是 pool.move borrow 入口取出 pool 内部 vault,vault.movebase_balancequote_balance split 出 Coin,同时返回 FlashLoan hot potato。PTB 中间可以组合 swap、套利或清算式动作,但最后必须把同一个方向的本金和 FlashLoan 一起交回 return 入口。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • 组合交易场景 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-12 没有跨交易债务状态的安全边界

返回本章

先看交易边界

这里先把问题放进同一笔交易里。“没有跨交易债务状态的安全边界”成立的前提是借出、使用、归还都在同一笔交易里完成;任何跨交易债务想象都应该立刻排除。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

DeepBookV3 闪电贷的安全边界来自 Move 能力系统和 vault 精确校验。它不依赖链下监控,也不需要在 state 里记录 borrower。只要交易成功,本金已经归还;交易失败,则所有状态回滚,借出也不存在。

风险主要在应用组合逻辑:

  • 中间 swap 滑点导致本金不够。
  • 错误池子或错误资产类型归还。
  • 把利润和本金 coin 合并后没有 split 成精确本金。
  • 借款数量超过 vault 可用余额。

因此 PTB 应先 dry run,并在组合路径中显式设置最小输出。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

归还校验关注三件事:pool id 是否匹配、借出资产方向是否匹配、归还数量是否等于 FlashLoan 记录的 amount。任意不一致都会让交易整体 abort,之前组合步骤的状态也不会落链。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • 没有跨交易债务状态的安全边界 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-13 借出再归还 PTB

返回本章

先看交易边界

读这一节时,把“借出再归还 PTB”放进一个 PTB 里推演。重点不是能不能借出资产,而是 hot potato 如何强迫调用方在同一交易中归还。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

最小 PTB 步骤:

  1. borrow_flashloan_base<Base, Quote>(&mut pool, amount) 得到 borrowed_coinflash_loan
  2. 可选对 borrowed_coin 做业务操作;最小示例不做任何操作。
  3. return_flashloan_base<Base, Quote>(&mut pool, borrowed_coin, flash_loan)

如果中间产生利润,例如得到 amount + profit 的 coin,应先 splitCoinsamount 作为还款 coin,剩余 coin 转回发送者或进入策略合约。

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

调用链是 pool.move borrow 入口取出 pool 内部 vault,vault.movebase_balancequote_balance split 出 Coin,同时返回 FlashLoan hot potato。PTB 中间可以组合 swap、套利或清算式动作,但最后必须把同一个方向的本金和 FlashLoan 一起交回 return 入口。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • 借出再归还 PTB 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch07-14 失败闪电贷练习

返回本章

先看交易边界

这里先把问题放进同一笔交易里。“失败闪电贷练习”成立的前提是借出、使用、归还都在同一笔交易里完成;任何跨交易债务想象都应该立刻排除。

源码入口

  • packages/deepbook/sources/pool.move:DeepBookV3 闪电贷唯一 Spot pool 入口,包含 borrow_flashloan_baseborrow_flashloan_quotereturn_flashloan_basereturn_flashloan_quote
  • packages/deepbook/sources/vault/vault.move:底层 FlashLoan hot potato、FlashLoanBorrowed 事件、pool/asset/amount 校验和 Vault 余额 split/join。
  • book/ch07/code/s01-flashloan-move-wrappers02-flashloan-ptbs03-flashloan-event-querys04-flashloan-indexer-row:本节对应的 wrapper、PTB、事件和表查询示例。

在一笔交易里走完

设计四个失败用例:

  1. 0:触发 EInvalidLoanQuantity = 3
  2. 借超过 vault 余额:base 触发 ENotEnoughBaseForLoan = 1,quote 触发 ENotEnoughQuoteForLoan = 2
  3. 从 pool A 借,在 pool B 还:触发 EIncorrectLoanPool = 4
  4. 借 base 还 quote,或还款数量不是精确本金:分别触发 EIncorrectTypeReturned = 5EIncorrectQuantityReturned = 6

安全边界:本章的闪电贷只指 DeepBookV3 Spot pool 闪电贷,入口在 pool.move,底层在 vault.move。不要把它和 packages/deepbook_margin 的普通借贷混淆;这里没有跨交易债务账户,也没有隔夜未还状态。

归还校验关注三件事:pool id 是否匹配、借出资产方向是否匹配、归还数量是否等于 FlashLoan 记录的 amount。任意不一致都会让交易整体 abort,之前组合步骤的状态也不会落链。

组合交易提醒

  • 借 base 必须用 return_flashloan_base 归还 base;借 quote 必须用 return_flashloan_quote 归还 quote。
  • FlashLoan 不能跨交易保存,PTB 中必须让 borrow 返回值最终被 return 消费。
  • 事件查询只把 FlashLoanBorrowed 当作成功交易的借出记录,不能推导出 Margin 债务状态。

动手检查

  • 失败闪电贷练习 的 pool.move 入口是哪一个,底层 vault.move 校验哪几个字段?
  • 如果 PTB abort,是 pool id、资产方向、归还数量、hot potato 未消费,还是中间组合步骤失败?
  • 这段逻辑是否仍明确区别 DeepBookV3 闪电贷和 deepbook_margin 普通借贷?

ch13 DeepBook Indexer、Server 与数据系统

本章目标

官方 DeepBookV3 Indexer 文档把 Indexer 定位为实时访问订单簿和交易数据的链下服务。它既提供 public service,也允许应用自建服务;选择哪一种,取决于延迟、可用性、定制需求和运维能力。

本章目标是把这个官方定位落成工程系统:理解为什么交易应用不能只依赖链上查询;掌握 crates/indexer 从 checkpoint 读取事件、解析事件、写入 PostgreSQL 的路径;掌握 crates/server 如何把数据库和链上只读调用封装为 REST API;最终能为交易终端、行情页、Margin 面板和风控系统设计可维护的数据服务。

官方数据基线

官方能力本章展开
Public DeepBookV3 indexer解释何时可以直接用 public endpoint,何时必须自建。
/get_pools、historical volume、OHLCV、user volume映射到池元数据、K 线、个人交易历史和资产精度处理。
Asset conversions所有返回 volume 都要用资产 decimals/scalar 解释,不能直接显示整数。
Margin events区分 DeepBook flashloans、Margin loan borrowed/repaid、liquidation 等不同生命周期。
/status 与服务健康机器人、做市和风控系统必须根据 checkpoint lag 降级或暂停。

本章学习阶梯

  • L1 先理解为什么交易应用不能只靠实时链上查询。
  • L2 读 checkpoint、event digest、handler、schema 和 Server route。
  • L3 设计订单、成交、闪电贷、Margin 事件的表和 API。
  • L5 处理 checkpoint lag、缓存一致性、部署和监控。

关键定义卡片

Indexer 的核心不是“查对象”,而是把事件落成读模型。以成交事件为例:

#![allow(unused)]
fn main() {
define_handler! {
    name: OrderFillHandler,
    processor_name: "order_fill",
    event_type: OrderFilled,
    db_model: OrderFill,
    table: order_fills,
    map_event: |event, meta| OrderFill {
        event_digest: meta.event_digest(),
        digest: meta.digest(),
        checkpoint: meta.checkpoint(),
        pool_id: event.pool_id.to_string(),
        maker_order_id: event.maker_order_id.to_string(),
        taker_order_id: event.taker_order_id.to_string(),
        price: event.price as i64,
        base_quantity: event.base_quantity as i64,
        quote_quantity: event.quote_quantity as i64,
        onchain_timestamp: event.timestamp as i64,
    }
}
}

这段定义告诉我们四件事:事件类型是 OrderFilled,目标表是 order_fills,主键语义来自 event_digest,时间窗口查询依赖 checkpoint 和 onchain timestamp。前端成交历史、K 线和账户流水都应该从事件语义设计,而不是从当前对象状态反推。

源码地图

小节目录

本章代码

  • code/s01-local-postgres/:本地 PostgreSQL 启动和数据库准备。
  • code/s02-run-indexer/:运行 DeepBook Indexer 的命令模板。
  • code/s03-query-events/:查询订单、成交、闪电贷和 Margin 事件。
  • code/s04-kline-builder/:从成交表生成 K 线的思路。
  • code/s05-api-client/:调用 DeepBook Server API。
  • code/s06-monitoring/:检查 /status 和 Prometheus 指标。

Move 高阶穿插点

  • Indexer 学习重点是事件语义:事件不是日志装饰,而是 Move 状态迁移留给链下系统的事实边界。
  • 设计表结构时先确认事件是否唯一、是否可重放、是否需要 transaction digest 和 event index 组成幂等键。
  • Server API 不应该伪造链上不存在的确定性,遇到延迟和重组风险要明确暴露数据时间。

常见错误

  • 把 public indexer 当作无限 SLA。生产机器人和风控系统需要明确延迟阈值和降级策略。
  • 把链上对象状态和 Indexer 落库状态视为完全同步。
  • 把 DeepBookV3 闪电贷表当成 Margin 借贷表。
  • 忘记给高频查询加时间窗口和分页。
  • 在迁移中修改大表字段但没有评估锁表时间。
  • Indexer 落后时继续运行依赖实时数据的机器人。

本章检查清单

  • 能说清 checkpoint、event、digest、event_digest 的关系。
  • 能找到 core DeepBook 和 Margin handlers 的注册位置。
  • 能解释 define_handler! 如何把事件写入数据库。
  • 能区分 flashloansloan_borrowed
  • 能设计交易终端所需的最小查询表。
  • 能使用 /status 判断 Indexer 是否健康。

进阶练习

  • order_fills 设计一个 1 分钟 K 线物化视图。
  • flashloans 写一个按池子统计每日借出数量的 SQL。
  • 设计一个当 checkpoint lag 超过阈值时自动暂停做市机器人的机制。

ch13-01 为什么交易应用不能只依赖链上查询

返回本章

先看数据问题

这一节从读模型而不是数据库表开始。围绕“为什么交易应用不能只依赖链上查询”,先问交易终端、机器人或风控系统要查什么,再看 handler 和 schema 如何支撑。

源码入口

落到读模型里

DeepBook 的链上对象适合保存最终可信状态,但交易产品还需要低延迟、可分页、可聚合的数据。订单簿页面需要最近成交、K 线、用户历史订单、订单状态、手续费、池子列表和 Margin 风险数据。如果每次都从 RPC 扫对象和事件,前端会面对三个问题:查询慢、分页困难、历史聚合成本高。

链下数据层的职责不是替代链上状态,而是把链上事件转成应用可查询的读模型。交易提交和资金安全仍由 Move 合约保证;Indexer 负责把 OrderFilledOrderUpdateFlashLoanBorrowedLoanBorrowedLiquidation 等事件落库。

交易终端需要的是可分页、可过滤、可聚合的读模型,而不是一次次扫描链上对象。Indexer 从 checkpoint 抽取成交、订单更新、余额和池子事件后写入 PostgreSQL,Server 再用 reader.rsorder_fillsbalancespool_prices 等表裁剪成前端 API。这里的核心边界是:链上对象仍是最终可信状态,数据库只是为查询体验和监控服务。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-02 Sui checkpoint、event 与 digest 模型

返回本章

先看数据问题

这里先从读模型需求开始。“Sui checkpoint、event 与 digest 模型”要回答的是链上事件如何变成可查询、可分页、可对账、可监控的读模型。

源码入口

落到读模型里

Indexer 的输入单位是 checkpoint。每个 checkpoint 包含一批交易,交易包含 sender、digest、effects、events 和输入对象。EventMeta 会从 checkpoint 和交易中提取:

  • digest:交易 digest。
  • sender:交易发送者。
  • checkpoint:checkpoint sequence number。
  • checkpoint_timestamp_ms:链上 checkpoint 时间。
  • package:触发 Move call 的 package。
  • event_index:事件在交易内的序号。

event_digest() 使用 digest + event_index 组成主键,解决一笔交易内多个同类事件的去重问题。

工程提醒:不要只用交易 digest 当事件主键。一笔交易可以发出多个 DeepBook 事件,尤其是订单、成交、返佣或组合交易场景。digest + event_index 才能稳定表达“这一条事件”。

EventMeta 把 checkpoint、高度时间戳、交易 digest 和 event digest 一起传给 handler。表以 event_digest 作为幂等键,并保留 checkpointcheckpoint_timestamp_ms,这样重放 fixture、排查交易回执和 API 时间窗口查询可以使用同一组定位字段。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-03 sui-indexer-alt 在本项目中的使用方式

返回本章

先看数据问题

这一节从读模型而不是数据库表开始。围绕“sui-indexer-alt 在本项目中的使用方式”,先问交易终端、机器人或风控系统要查什么,再看 handler 和 schema 如何支撑。

源码入口

落到读模型里

crates/indexer/src/main.rs 使用 Indexer::new(...) 创建索引器,然后对每类事件注册 concurrent_pipeline。核心包由 --packages deepbook 控制,Margin 包由 --packages deepbook-margin 控制。

生产模式要求传入 --env testnet|mainnet。Sandbox 模式允许指定自定义 package id 和本地 checkpoint 路径,适合本地部署 DeepBook 后做集成测试。

本项目把 sui-indexer-alt 当作 checkpoint 消费框架使用,业务逻辑集中在 handlers。新增事件时通常先在 handlers/mod.rs 注册 handler,再补 Diesel model 和 migration,最后用 checkpoint fixture 做 snapshot,避免 pipeline 能启动但字段落库错误。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-04 Indexer 启动参数

返回本章

先看数据问题

这里先从读模型需求开始。“Indexer 启动参数”要回答的是链上事件如何变成可查询、可分页、可对账、可监控的读模型。

源码入口

落到读模型里

默认数据库地址来自 database_url 参数,默认值是 postgres://postgres:postgrespw@localhost:5432/deepbook。常用启动方式:

DATABASE_URL="postgresql://postgres:postgrespw@localhost:5432/deepbook" \
cargo run --package deepbook-indexer -- --env testnet --packages deepbook deepbook-margin

关键参数:

  • --env:选择 testnetmainnet
  • --packages:选择 deepbookdeepbook-margin 或两者同时启用。
  • --metrics-address:Prometheus 指标地址,默认 0.0.0.0:9184
  • sandbox 子命令:用本地或测试网自定义包进行索引。

启动参数决定网络、数据库连接、起始 checkpoint 和 pipeline 组合。生产环境要把 RPC、PostgreSQL、metrics 端口和重放起点显式写入部署清单,避免容器重启后从错误网络或错误 checkpoint 继续消费。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-05 Handler 与 map_event 模式

返回本章

先看数据问题

这一节从读模型而不是数据库表开始。围绕“Handler 与 map_event 模式”,先问交易终端、机器人或风控系统要查什么,再看 handler 和 schema 如何支撑。

源码入口

关键定义

以成交事件 handler 为例:

#![allow(unused)]
fn main() {
define_handler! {
    name: OrderFillHandler,
    processor_name: "order_fill",
    event_type: OrderFilled,
    db_model: OrderFill,
    table: order_fills,
    map_event: |event, meta| OrderFill {
        event_digest: meta.event_digest(),
        digest: meta.digest(),
        sender: meta.sender(),
        checkpoint: meta.checkpoint(),
        checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(),
        pool_id: event.pool_id.to_string(),
        maker_order_id: event.maker_order_id.to_string(),
        taker_order_id: event.taker_order_id.to_string(),
        price: event.price as i64,
        base_quantity: event.base_quantity as i64,
        quote_quantity: event.quote_quantity as i64,
        onchain_timestamp: event.timestamp as i64,
    }
}
}

这段定义把 Indexer 的职责说清楚了:event_type 指链上事件,db_model 指 Rust/Diesel model,table 指 PostgreSQL 表,map_event 只做字段映射。不要在 handler 里做复杂业务聚合;K 线、用户历史、订单状态聚合应该在 Server 查询层或物化任务中完成。

落到读模型里

define_handler! 宏把重复逻辑固定下来:遍历 checkpoint 中的交易,筛选 DeepBook 相关交易,匹配事件类型,用 BCS 反序列化,再映射成数据库 model,最后批量 insert。

OrderFillHandler 为例,OrderFilled 事件会被映射成 OrderFill

  • pool_id:成交所在池子。
  • maker_order_idtaker_order_id:双方订单。
  • pricebase_quantityquote_quantity:成交价格和数量。
  • maker_feetaker_fee:双方费用。
  • maker_balance_manager_idtaker_balance_manager_id:双方交易账户。

这就是行情、成交历史和用户交易记录的基础表。

define_handler! 把“事件类型、目标表、字段映射”放在一个局部声明中。map_event 不应该做复杂查询,它只把链上事件和 EventMeta 组合成 Diesel insert model;需要聚合的订单簿、K 线和用户历史应放到 Server 查询或离线物化任务。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-06 Core DeepBook handlers

返回本章

先看数据问题

链上事件本身不适合直接支撑交易终端。用户要看订单状态、成交流、余额历史和行情,机器人还要按 checkpoint 延迟决定是否暂停交易。这些都需要读模型。

所以这一节不从 PostgreSQL 表名开始,而是从 handler 的职责开始:每个 handler 把一种链上事件翻译成一张可以查询、分页、对账和监控的表。

源码入口

关键定义

Indexer 的核心 handler 不是手写一堆重复 trait impl,而是通过 define_handler! 宏声明事件类型、目标表和字段映射。下面是成交事件的典型形态。

#![allow(unused)]
fn main() {
define_handler! {
    name: OrderFillHandler,
    processor_name: "order_fill",
    event_type: OrderFilled,
    db_model: OrderFill,
    table: order_fills,
    map_event: |event, meta| OrderFill {
        event_digest: meta.event_digest(),
        digest: meta.digest(),
        sender: meta.sender(),
        checkpoint: meta.checkpoint(),
        checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(),
        package: meta.package(),
        pool_id: event.pool_id.to_string(),
        maker_order_id: event.maker_order_id.to_string(),
        taker_order_id: event.taker_order_id.to_string(),
        price: event.price as i64,
        taker_is_bid: event.taker_is_bid,
        base_quantity: event.base_quantity as i64,
        quote_quantity: event.quote_quantity as i64,
        onchain_timestamp: event.timestamp as i64,
    }
}
}

这段映射体现了读模型的取舍:event_digestdigestcheckpoint 是可追溯性字段;pool_id、order id 和数量字段是业务查询字段。写查询 API 时不要只按业务字段建索引,还要保留 checkpoint 维度,便于补扫、对账和延迟监控。

落到读模型里

核心 DeepBook pipeline 可以按产品问题来记:

  • OrderFillHandler:写入 order_fills
  • OrderUpdateHandler:写入 order_updates,用于订单状态。
  • BalancesHandler:写入 balances,用于 BalanceManager 资金历史。
  • PoolCreatedHandler:写入池子创建事件。
  • PoolPriceHandler:写入参考价格。
  • TradeParamsUpdateHandler:写入交易参数变化。
  • StakesHandlerRebatesHandlerRebatesV2Handler:写入 DEEP stake 和返佣数据。
  • BookParamsUpdatedHandler:写入 tick、lot 等 book 参数变化。

交易终端通常至少需要 order_fillsorder_updatesbalancespoolspool_pricesorder_fills 生成成交流和 K 线,order_updates 跟踪订单生命周期,balances 解释 BalanceManager 维度的资金变化,pool_prices 支撑行情展示和风控提示。

数据系统判断

  • 查询接口不要只按业务字段设计,还要带 checkpoint 或时间窗口,方便补扫和对账。
  • 实时交易应用必须同时看 /status、checkpoint lag 和数据时间戳,再决定是否继续自动交易。
  • Flash Loan、Margin loan 和普通成交不要混表解释;它们的生命周期完全不同。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-07 Flash Loan handler 与 flashloans

返回本章

先看数据问题

这一节从读模型而不是数据库表开始。围绕“Flash Loan handler 与 flashloans 表”,先问交易终端、机器人或风控系统要查什么,再看 handler 和 schema 如何支撑。

源码入口

关键定义

链上事件定义:

public struct FlashLoanBorrowed has copy, drop {
    pool_id: ID,
    borrow_quantity: u64,
    type_name: TypeName,
}

Indexer 映射:

#![allow(unused)]
fn main() {
define_handler! {
    name: FlashLoanHandler,
    processor_name: "flash_loan",
    event_type: FlashLoanBorrowed,
    db_model: Flashloan,
    table: flashloans,
    map_event: |event, meta| Flashloan {
        event_digest: meta.event_digest(),
        digest: meta.digest(),
        checkpoint: meta.checkpoint(),
        checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(),
        pool_id: event.pool_id.to_string(),
        borrow_quantity: event.borrow_quantity as i64,
        borrow: true,
        type_name: event.type_name.to_string(),
    }
}
}

注意这里没有 FlashLoanReturned 事件。成功归还由同一交易不 abort 保证,表里只记录 borrow 事件。borrow: true 不是债务方向标记,而是当前 handler 固定写入的借出事件字段。

落到读模型里

DeepBookV3 闪电贷事件来自 packages/deepbook/sources/vault/vault.moveFlashLoanBorrowed。Indexer 中的 FlashLoanHandler 把它写入 flashloans 表:

  • pool_id:借出资产所在池子。
  • borrow_quantity:借出数量。
  • type_name:借出的资产类型。
  • borrow:当前 handler 固定写为 true,表示借出事件。

注意:链上 FlashLoan hot potato 会强制同一交易归还资产,因此表里记录的是借出事件,不代表存在跨交易债务。不要把它和 Margin 的 loan_borrowedloan_repaid 混为一类。

需要特别区分两类借贷语义:flashloans 记录 DeepBook vault 的 FlashLoanBorrowed 借出事件,hot potato 保证同一 PTB 内归还,因此它不是跨交易负债;Margin 的 loan_borrowedloan_repaid 记录用户在 MarginPool 中形成和偿还的债务生命周期,风险率、利息和清算都应读取 Margin 表。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-08 Margin handlers

返回本章

先看数据问题

这里先从读模型需求开始。“Margin handlers”要回答的是链上事件如何变成可查询、可分页、可对账、可监控的读模型。

源码入口

关键定义

Margin 借款事件的 handler 和核心成交 handler 结构一致,但业务主键不同:这里围绕 margin_manager_idmargin_pool_id、loan amount 和 shares 建读模型。

#![allow(unused)]
fn main() {
define_handler! {
    name: LoanBorrowedHandler,
    processor_name: "loan_borrowed",
    event_type: LoanBorrowedEvent,
    db_model: LoanBorrowed,
    table: loan_borrowed,
    map_event: |event, meta| LoanBorrowed {
        event_digest: meta.event_digest(),
        digest: meta.digest(),
        sender: meta.sender(),
        checkpoint: meta.checkpoint(),
        checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(),
        package: meta.package(),
        margin_manager_id: event.margin_manager_id.to_string(),
        margin_pool_id: event.margin_pool_id.to_string(),
        loan_amount: event.loan_amount as i64,
        loan_shares: event.loan_shares as i64,
        onchain_timestamp: event.timestamp as i64,
    }
}
}

借款读模型必须同时保存 amount 和 shares。amount 解释这次事件的即时规模,shares 才能和后续利息累计后的债务余额对应起来。Margin 面板如果只展示历史 amount,会低估长期仓位的真实 debt。

落到读模型里

Margin 事件覆盖账户、借贷、清算、抵押、配置和 TPSL:

  • MarginManagerCreatedHandler:Margin 账户创建。
  • LoanBorrowedHandlerLoanRepaidHandler:普通借贷生命周期。
  • AssetSuppliedHandlerAssetWithdrawnHandler:供应和退出 MarginPool。
  • DepositCollateralHandlerWithdrawCollateralHandler:抵押品变化。
  • LiquidationHandler:清算事件。
  • ConditionalOrderAddedHandlerConditionalOrderExecutedHandlerConditionalOrderCancelledHandler:TPSL 条件订单。
  • InterestParamsUpdatedHandlerMarginPoolConfigUpdatedHandler:利率和风险参数更新。

Margin 面板的关键查询是:当前债务、抵押品、历史借还、风险参数、清算历史。

Margin handlers 维护的是可审计的债务和抵押事件流。loan_borrowed 表示借款增加,loan_repaid 表示债务减少;它们和 deposit_collateralwithdraw_collateralliquidation 一起才能解释仓位健康度,不能用 DeepBook flashloans 表替代。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-09 Schema migrations 与 Diesel model

返回本章

先看数据问题

这一节从读模型而不是数据库表开始。围绕“Schema migrations 与 Diesel model”,先问交易终端、机器人或风控系统要查什么,再看 handler 和 schema 如何支撑。

源码入口

落到读模型里

crates/schema/migrations 是数据库事实来源。初始迁移创建了 order_updatesorder_fillsflashloanspool_pricesbalancestrade_params_update 等表。后续迁移增加 Margin 表、OHCLV、索引、返佣 v2、池子快照等。

生产上必须把 migrations 当成协议兼容层管理:事件字段变化、表字段变化、API 响应变化要一起评估。Indexer 启动时会执行 pending migrations,因此部署新版本前要先验证迁移可重复执行、可回滚、不会锁表过久。

迁移文件定义数据库事实,schema.rsmodels.rs 则让 Indexer/Server 在编译期使用同一套字段。修改大表时要同步考虑主键、唯一约束、索引和回填成本,尤其是 event_digest 幂等键与按 checkpoint_timestamp_ms 查询的复合索引。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-10 PostgreSQL 查询模式

返回本章

先看数据问题

这里先从读模型需求开始。“PostgreSQL 查询模式”要回答的是链上事件如何变成可查询、可分页、可对账、可监控的读模型。

源码入口

落到读模型里

常见查询模式:

  • 最近成交:按 pool_idcheckpoint_timestamp_ms 倒序查 order_fills
  • K 线:按时间桶聚合 order_fills 的 price、base quantity、quote quantity。
  • 用户订单:按 balance_manager_idorder_updates
  • 用户成交:按 maker 或 taker balance_manager_idorder_fills
  • 闪电贷统计:按 pool_idtype_name 聚合 flashloans
  • Margin 借贷:按 margin_manager_idloan_borrowedloan_repaid

这些查询都应有复合索引支撑。读多写多的表要避免前端任意排序,API 应限制时间窗口和分页大小。

Server 的高频查询都应限制池子、账户和时间窗口。成交流、用户历史和订单列表不能开放无界扫描;分页字段最好与索引顺序一致,例如按 checkpoint_timestamp_ms 倒序、再用 event_digest 或订单 id 做稳定游标。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-11 Server REST API

返回本章

先看数据问题

这一节从读模型而不是数据库表开始。围绕“Server REST API”,先问交易终端、机器人或风控系统要查什么,再看 handler 和 schema 如何支撑。

源码入口

落到读模型里

crates/server/src/server.rs 暴露 DeepBook 和 Margin 数据 API。核心路径包括:

  • /get_pools:池子列表。
  • /ticker:行情摘要。
  • /trades/:pool_name:成交记录。
  • /order_updates/:pool_name:订单更新。
  • /orders/:pool_name/:balance_manager_id:用户订单。
  • /orderbook/:pool_name:二级订单簿。
  • /ohclv/:pool_name:K 线。
  • /loan_borrowed/loan_repaid/liquidation:Margin 事件。
  • /status:Indexer 健康状态。

Server 同时会做链上只读调用,例如订单簿 level2、费用参数、DEEP supply。应用要区分数据库读模型和链上实时读模型的延迟差异。

REST 层把数据库读模型、链上只读 RPC 和管理端点拆开。行情、历史成交和 Margin metrics 走只读 reader;需要触发写操作或管理行为时应经过明确鉴权、限流和错误码映射,避免前端把内部错误直接展示成交易失败原因。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-12 /status 与 checkpoint lag

返回本章

先看数据问题

这里先从读模型需求开始。“/status 与 checkpoint lag”要回答的是链上事件如何变成可查询、可分页、可对账、可监控的读模型。

源码入口

落到读模型里

/status 返回每个 pipeline 的 indexed checkpoint、latest on-chain checkpoint、checkpoint lag 和 time lag。风控系统至少要监控:

  • status 是否为 OK
  • max_checkpoint_lag 是否超过阈值。
  • max_time_lag_seconds 是否超过阈值。
  • max_lag_pipeline 是哪个 pipeline。

当 Indexer 落后时,前端应提示数据延迟,自动交易系统应降低频率或暂停依赖历史数据的策略。

EventMeta 把 checkpoint、高度时间戳、交易 digest 和 event digest 一起传给 handler。表以 event_digest 作为幂等键,并保留 checkpointcheckpoint_timestamp_ms,这样重放 fixture、排查交易回执和 API 时间窗口查询可以使用同一组定位字段。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-13 Prometheus metrics

返回本章

先看数据问题

这一节从读模型而不是数据库表开始。围绕“Prometheus metrics”,先问交易终端、机器人或风控系统要查什么,再看 handler 和 schema 如何支撑。

源码入口

落到读模型里

Indexer 默认暴露 Prometheus 指标。需要纳入生产告警的指标包括数据库连接、pipeline 处理延迟、checkpoint lag、错误数、Server 请求延迟和 HTTP 5xx。指标只是告警入口,排障还需要保留 digest、checkpoint、pipeline name 和数据库写入错误。

Prometheus 指标要覆盖 API 延迟、数据库请求成功率、Margin 轮询错误和业务派生值。告警阈值应结合 /status 使用:单次 RPC 慢不一定影响交易,但 checkpoint lag 扩大说明读模型已不适合驱动机器人。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-14 订单簿缓存、K 线与用户历史

返回本章

先看数据问题

这里先从读模型需求开始。“订单簿缓存、K 线与用户历史”要回答的是链上事件如何变成可查询、可分页、可对账、可监控的读模型。

源码入口

落到读模型里

订单簿实时展示可以结合链上 level2 调用和本地缓存。K 线不应每次请求都扫全表,应该按固定周期预聚合或使用物化视图。用户历史订单需要同时合并 order_updatesorder_fills,因为订单更新描述状态,成交表描述实际成交。

订单簿快照、成交流、K 线和用户历史来自不同粒度的数据。order_fills 适合生成成交流和 OHLCV,订单簿实时层更适合短 TTL 缓存;用户历史必须按 BalanceManager 或地址过滤,避免把全市场成交当作用户成交。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-15 Docker 部署

返回本章

先看数据问题

这一节从读模型而不是数据库表开始。围绕“Docker 部署”,先问交易终端、机器人或风控系统要查什么,再看 handler 和 schema 如何支撑。

源码入口

落到读模型里

生产拓扑通常包含 PostgreSQL、Indexer、Server、Prometheus、Grafana 和前端。Indexer 只负责写库,Server 只负责读库和链上只读调用,两者应独立扩缩容。数据库是关键状态,不要让应用容器启动脚本隐式清空数据。

容器部署时 Indexer、Server 和 PostgreSQL 是三个不同故障域。Indexer 需要稳定 RPC 和写库权限,Server 需要只读连接池和 metrics 端口,迁移最好作为独立步骤执行,避免服务启动并发抢锁。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-16 数据一致性

返回本章

先看数据问题

这里先从读模型需求开始。“数据一致性”要回答的是链上事件如何变成可查询、可分页、可对账、可监控的读模型。

源码入口

落到读模型里

事件写入必须幂等。当前 handler 使用 event_digest 作为主键,并 on_conflict_do_nothing,这允许 pipeline 重放 checkpoint 而不重复写入。需要注意的是:如果事件映射逻辑变更,旧数据不会自动重算,必须设计数据回填或重建流程。

数据一致性依赖幂等写入、checkpoint 顺序和可观测的落后程度。API 返回的数据应携带或内部记录 checkpoint 范围,前端确认交易时用 digest 对齐链上执行结果,再等待 Indexer 追上对应 checkpoint。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-17 前端如何消费 API

返回本章

先看数据问题

这一节从读模型而不是数据库表开始。围绕“前端如何消费 API”,先问交易终端、机器人或风控系统要查什么,再看 handler 和 schema 如何支撑。

源码入口

落到读模型里

前端应把 API 分成三类:

  • 快速刷新:ticker、orderbook、recent trades。
  • 用户相关:orders、fills、balances、Margin positions。
  • 低频配置:pools、fees、assets、risk params。

每类使用不同缓存 TTL。用户下单后的状态不应只靠轮询,要结合交易 digest 查询、Indexer API 和链上对象状态做确认。

前端消费 API 要把“交易已上链”和“读模型已刷新”拆成两个状态。提交 PTB 后先展示 digest 和链上确认,再轮询历史成交、订单更新或 /status;当 checkpoint lag 超阈值时,应提示数据延迟而不是重复发交易。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-18 自建 Indexer 与公共 API 的取舍

返回本章

先看数据问题

这里先从读模型需求开始。“自建 Indexer 与公共 API 的取舍”要回答的是链上事件如何变成可查询、可分页、可对账、可监控的读模型。

源码入口

落到读模型里

早期应用可以使用公共 API 快速开发,但专业交易、做市、清算和风控系统应自建 Indexer。自建的价值是可控延迟、可控表结构、可回放、可审计。代价是数据库运维、迁移管理、链上数据兼容和监控成本。

公共 API 适合低频查询和原型,自建 Indexer 适合机器人、风控和审计留痕。取舍点包括延迟、可用性、历史保留、查询自由度和故障责任;一旦应用需要按业务字段聚合或回放,就应准备自建数据层。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-19 本地启动路径

返回本章

先看数据问题

这一节从读模型而不是数据库表开始。围绕“本地启动路径”,先问交易终端、机器人或风控系统要查什么,再看 handler 和 schema 如何支撑。

源码入口

落到读模型里

本地最小路径:

  1. 启动 PostgreSQL。
  2. 设置 DATABASE_URL
  3. MystenLabs/deepbookv3 运行 cargo run --package deepbook-indexer -- --env testnet --packages deepbook
  4. 启动 deepbook-server
  5. 调用 /status/get_pools/trades/:pool_name 验证数据流。

GitHub 源码应先启动 PostgreSQL 和 migration,再启动 Indexer 追 checkpoint,最后启动 Server 验证 /status 和一个真实查询。调试时固定网络、package id、数据库 URL 和起始 checkpoint,才能让不同 worker 的结果可复现。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch13-20 行情 API 聚合服务

返回本章

先看数据问题

这里先从读模型需求开始。“行情 API 聚合服务”要回答的是链上事件如何变成可查询、可分页、可对账、可监控的读模型。

源码入口

落到读模型里

行情聚合服务不直接修改链上状态,只读取 Server API 或本地 PostgreSQL,并生成适合前端的数据结构:ticker、K 线、深度、最近成交、资金费率或 Margin 利率。这个服务的核心是缓存策略和降级策略,而不是复杂业务逻辑。

行情聚合服务应把成交、价格、成交量和池子元数据压成前端需要的 ticker 响应。聚合层要声明时间窗口、刷新频率和缺失数据策略,避免 UI 把短期无成交误读成价格为零。

数据系统判断

  • 先确认本节涉及的事件名、handler、目标表和 API 端点,避免把链上对象状态与读模型混用。
  • 查询设计必须带池子、账户、checkpoint 或时间窗口,并说明分页和索引依据。
  • 对实时应用要同时检查 /status、checkpoint lag 和数据时间戳,再决定是否交易、降级或告警。
  • 涉及 Flash Loan 或 Margin 时,明确 flashloansloan_borrowedloan_repaid 的语义差异。

动手检查

  • 本节对应的 handler、表名和 Server 查询入口分别是什么?
  • 这个数据在链上、Indexer、PostgreSQL、Server 缓存和前端之间可能出现哪些延迟或不一致?
  • 如果用于机器人或风控,应该设置什么 checkpoint lag、分页窗口和降级策略?

ch08 DeepBook Margin 协议源码精读

本章目标

官方文档把 DeepBook Margin 定位为 DeepBookV3 上的杠杆交易扩展:用户通过借入资金放大买卖能力,同时承担清算、利率和 oracle 风险。本章不从“多了哪些函数”开始,而是从风险系统开始读源码。

读完本章后,读者应该能解释 Margin 如何把抵押品、借贷池、DeepBook Spot 订单簿和清算流程连成一个杠杆交易系统;能读懂 MarginRegistryMarginManagerMarginPool<Asset>pool_proxymargin_stateprotocol_configtpslliquidation_vault 的职责边界;也能根据风险率、利率、预言机价格和清算参数,推导一次开仓、还款或清算交易的状态变化。

官方风险基线

Margin 章节必须始终围绕风险写,而不是围绕接口列表写。

官方主题本章落地方式
Leveraged positionsch09 讲用户路径,ch08 讲源码和状态边界。
Risk management and liquidationch08-06、ch08-13、ch08-16 讲风险率、清算和阈值。
Collateral flexibilitych08-03、ch09-04 讲 MarginManager 如何包装抵押品和 BalanceManager。
Interest accrualch08-08、ch08-09、ch08-10 讲 shares、utilization、protocol spread 和费用分配。
Oracle riskch08-11 讲 stale price、confidence、EWMA 和交易前检查。

官方主网合同信息会随升级变化。本章解释对象和状态机时以源码为主;写对象 ID、版本、风险参数和池列表时,应回到 DeepBook Margin contract information 核对最新值。

本章学习阶梯

  • L2 先理解 Margin 与 Spot 的关系:借贷和交易不是同一个状态机。
  • L3 读 MarginManagerMarginPool、oracle、risk ratio 和 TPSL。
  • L4 能解释借款、还款、清算和风险率变化的完整路径。
  • L5 能为 Margin 协议做安全边界和异常路径审查。

关键定义卡片

Margin 的用户账户不是普通仓位对象,而是一个包装了 DeepBook 账户能力的共享对象:

public struct MarginManager<phantom BaseAsset, phantom QuoteAsset> has key {
    id: UID,
    owner: address,
    deepbook_pool: ID,
    margin_pool_id: Option<ID>,
    balance_manager: BalanceManager,
    deposit_cap: DepositCap,
    withdraw_cap: WithdrawCap,
    trade_cap: TradeCap,
    borrowed_base_shares: u64,
    borrowed_quote_shares: u64,
    take_profit_stop_loss: TakeProfitStopLoss,
    extra_fields: VecMap<String, u64>,
}

这段定义解释了 Margin 为什么必须先懂 Spot:MarginManager 内部直接持有 BalanceManager 和 cap。Margin 交易通过这些能力调用 DeepBook 池,同时额外检查借贷 shares、oracle 和风险率。

借贷池定义:

public struct MarginPool<phantom Asset> has key, store {
    id: UID,
    vault: Balance<Asset>,
    state: State,
    config: ProtocolConfig,
    protocol_fees: ProtocolFees,
    positions: PositionManager,
    allowed_deepbook_pools: VecSet<ID>,
    rate_limiter: RateLimiter,
    extra_fields: VecMap<String, u64>,
}

vault 保存供应资产,state 记录 shares 和利息,allowed_deepbook_pools 控制哪些 Spot 池可以接入这个借贷资产。Margin 的风险不是单个字段能解释的,必须把 manager、pool、registry 和 oracle 放在一起读。

源码地图

小节目录

本章代码

  • code/s01-margin-source-map/:生成 Margin 模块依赖图,帮助定位 registry、manager、pool、proxy、oracle、TPSL、liquidation vault 的调用关系。
  • code/s02-risk-ratio-calculator/:用示例资产、债务和价格计算风险率,验证 min borrow、min withdraw 和 liquidation 阈值。
  • code/s03-interest-accrual/:模拟 margin_state.updateprotocol_config.interest_rate
  • code/s04-liquidation-scenario/:构造清算输入,推导 repay amount、用户奖励、池奖励和可能坏账。

Move 高阶穿插点

  • Margin 是复合状态机:抵押、借款、订单、风险率、oracle、清算每一层都有自己的不变量。
  • MarginManager 时先找它如何包装 BalanceManager,再看借贷 shares 和 TPSL 状态。
  • oracle 相关代码要同时读价格值、decimals、时间戳和最大年龄,缺一个就不能做生产风控。

常见错误

  • MarginPool.supply 理解成给自己加保证金。供应到 MarginPool 是给借贷池提供流动性;用户保证金应存入 MarginManager.deposit
  • 直接调用 DeepBook Pool 下单。Margin 仓位必须通过 pool_proxy,否则价格保护、manager 权限和 reduce-only 逻辑不会生效。
  • 用借款金额替代 borrow shares。链上债务随利息增长,真实金额要通过 borrow_shares_to_amount 计算。
  • 只展示杠杆倍数,不展示风险率、利率、清算价和 oracle freshness。出版社级内容必须把用户能亏在哪里讲清楚。
  • 忽略 oracle freshness。前端展示可以用 unsafe 读接口,但执行交易必须按链上安全 oracle 检查。
  • 把清算奖励当成固定转账。liquidate 会根据目标风险率、资产覆盖程度和 repay coin 数量动态计算。

本章检查清单

  • 能说明 MarginRegistry 中 PoolConfig 的每个风险参数如何影响借款、提款和清算。
  • 能说明 MarginManager 为什么同时持有 BalanceManager 和三种 cap。
  • 能说明 supply shares 与 borrow shares 为什么不会随每秒利息直接改变。
  • 能追踪一次 borrow_quote 从 MarginPool vault 到 BalanceManager 的资金变化。
  • 能解释 pool_proxy 对限价单和市价单做价格保护的差异。
  • 能解释 TPSL 为什么设计成 permissionless execute。
  • 能推导一次 partial liquidation 的 repay amount 和奖励。

进阶练习

  • 画出 MarginManager<SUI, USDC> 开 2 倍 SUI 多头的对象图和资金流。
  • 修改 s02-risk-ratio-calculator 的价格输入,观察风险率从 2.0 下降到 1.1 以下时哪些操作会失败。
  • s03-interest-accrual 比较利用率 40%、80%、95% 时的年化利率和一天后 borrow amount。
  • 根据 liquidation_vault.move 设计一个清算机器人事件日志表,字段至少包含 manager、pool、repay asset、risk ratio、pool default。

ch08-01 Margin 与 Spot 的关系

返回本章

先看风险边界

读“Margin 与 Spot 的关系”时先问:这一步会让账户风险变大还是变小,谁有权继续执行,失败时应该归因到价格、债务、池配置还是对象权限。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

读风险控制面

DeepBook Margin 不重写订单簿。现货撮合仍由 DeepBook Pool 完成,Margin 只是在用户和 Pool 之间加了一层账户、借贷和风险控制。

资金路径分四段:

  • 抵押:用户把 base、quote 或 DEEP 存入 MarginManager,内部调用 BalanceManager.deposit_with_cap。入口是 margin_manager.movedeposit<BaseAsset, QuoteAsset, DepositAsset>
  • 借贷:用户从 MarginPool<BaseAsset>MarginPool<QuoteAsset> 借入资产,借来的 coin 立即存入同一个 MarginManager。入口是 borrow_baseborrow_quote
  • 交易:pool_proxy.move 使用 MarginManager 持有的 TradeCap 调用 DeepBook Pool 的 place_limit_orderplace_market_ordercancel_orderwithdraw_settled_amounts
  • 清算:当仓位风险率低于清算阈值时,清算方拿债务资产调用 margin_manager::liquidate,协议取消挂单、偿还部分或全部债务,并把用户资产按奖励规则转给清算方和借贷池。

这不是 DeepBookV3 闪电贷。DeepBook 闪电贷是 Pool 内的原子借还;Margin 借贷是 MarginPool<Asset> 的持续债务,债务以 borrow shares 记录,会随时间计息。

工程旁白

Margin 的核心边界是“债务生命周期”。DeepBook Pool 只看到一个带 TradeCap 的 BalanceManager 在下单;借款是否允许、借款后风险率是否足够、债务如何计息,全部在 Margin 模块内完成。应用侧如果绕过 pool_proxy 直接调用现货 Pool,就失去了价格保护、reduce-only 和 manager 权限约束。

和 DeepBookV3 闪电贷相比,Margin 借贷不会要求在同一笔 PTB 内归还。用户借入后会得到 borrow shares,后续利息由 margin_state 把 shares 换算成实时 debt amount;因此 UI、Indexer 和清算机器人都必须展示“债务资产”和“随时间增长的债务金额”。

风控判断

  • 不要把 MarginPool.supply 作为用户保证金入口;保证金进入 MarginManager.deposit,供应资产进入借贷池 vault。
  • 所有 Margin 下单都经过 pool_proxy,并在交易前重新读取 registry、oracle 和 pool 状态。
  • 产品文案要把“杠杆借贷”与“闪电贷”分开,避免用户以为仓位会在交易结束时自动归还。

动手检查

  • 这笔交易是否创建了持续债务,还是只是 DeepBook Pool 内的原子闪电贷?
  • 下单路径是否经过 pool_proxy 并携带正确的 manager、registry、pool 和 oracle 对象?
  • 风险率降低时,用户应该补抵押、还款、reduce-only 交易还是等待清算?

ch08-02 MarginRegistry

返回本章

先看风险边界

这里先把问题放到风险控制面里。“MarginRegistry”不是在 DeepBook 旁边加一层 UI,而是把 registry、manager、oracle、borrow/supply state 接进同一条链上风控路径。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

关键定义

MarginRegistry 外层是 key object,真实配置在 Versioned 里的 MarginRegistryInner。这让包升级和版本门禁可以独立于 registry 对象 ID 演进。

public struct MarginRegistry has key {
    id: UID,
    inner: Versioned,
}

public struct MarginRegistryInner has store {
    registry_id: ID,
    allowed_versions: VecSet<u64>,
    pool_registry: Table<ID, PoolConfig>,
    margin_pools: Table<TypeName, ID>,
    margin_managers: Table<address, VecSet<ID>>,
    allowed_maintainers: VecSet<ID>,
    allowed_pause_caps: VecSet<ID>,
}

public struct PoolConfig has copy, drop, store {
    base_margin_pool_id: ID,
    quote_margin_pool_id: ID,
    risk_ratios: RiskRatios,
    user_liquidation_reward: u64,
    pool_liquidation_reward: u64,
    enabled: bool,
    extra_fields: VecMap<String, u64>,
}

public struct RiskRatios has copy, drop, store {
    min_withdraw_risk_ratio: u64,
    min_borrow_risk_ratio: u64,
    liquidation_risk_ratio: u64,
    target_liquidation_risk_ratio: u64,
}

应用侧要把 PoolConfig 当成交易对级风控真值:同一个 MarginPool 可以服务多个 DeepBook Pool,但每个交易对的 base/quote 池、清算奖励和风险率都来自 registry,而不是来自前端配置文件。

读风险控制面

MarginRegistry 是所有 Margin 交易的控制平面。结构体在 margin_registry.move 中定义:

  • allowed_versions:允许调用的 Margin package 版本。init 默认写入 margin_constants::margin_version()
  • pool_registry:DeepBook Pool ID 到 PoolConfig 的映射。
  • margin_pools:资产类型 TypeName 到对应 MarginPool<Asset> ID 的映射。
  • margin_managers:owner 地址到其 MarginManager ID 集合的映射。
  • allowed_maintainersallowed_pause_caps:维护者和暂停能力白名单。

PoolConfig 是每个 DeepBook Pool 的 Margin 风控配置,包含:

  • base_margin_pool_idquote_margin_pool_id:这个交易对可使用的 base/quote 借贷池。
  • RiskRatiosmin_withdraw_risk_ratiomin_borrow_risk_ratioliquidation_risk_ratiotarget_liquidation_risk_ratio
  • user_liquidation_rewardpool_liquidation_reward:清算者和借贷池奖励,使用 9 位小数定点数。
  • enabled:是否允许常规 Margin 交易。

应用授权分两层:DeepBook 核心需要授权 Margin app,Margin 侧还要注册并启用具体 DeepBook Pool。部署脚本 scripts/transactions/marginPrep.ts 展示了顺序:authorizeMarginApp、创建 Pyth 配置、创建 MarginPool、registerDeepbookPoolenableDeepbookPoolenableDeepbookPoolForLoan

工程旁白

MarginRegistry 是应用读取风控参数的首要来源。前端展示池状态时,不应把 DeepBook Pool ID、base/quote MarginPool ID 和风险率配置写死;这些值可能通过维护者交易被暂停、更新或重新授权。

借款和提款失败经常不是余额不足,而是 registry 中的池没有启用、版本不在 allowed list、DeepBook Pool 没有被对应 MarginPool 授权。交易前模拟要把这些配置状态作为独立检查项输出。

风控判断

  • 把 PoolConfig 当作链上真值,SDK 配置只保存对象 ID 和类型,不缓存风险阈值作为执行依据。
  • 部署或测试环境初始化时按 marginPrep.ts 的顺序执行,先授权 app,再创建池和注册交易对。
  • 错误提示要区分 pool_enabled 失败、loan 未启用、版本不允许和风险率不足。

动手检查

  • 当前 DeepBook Pool 的 base/quote MarginPool ID 来自 registry 还是本地配置?
  • min_borrow_risk_ratioliquidation_risk_ratio 分别在哪些操作中被检查?
  • 如果 Pool 被暂停,用户还能通过哪些 reduce-only 路径降低风险?

ch08-03 MarginManager

返回本章

先看风险边界

读“MarginManager”时先问:这一步会让账户风险变大还是变小,谁有权继续执行,失败时应该归因到价格、债务、池配置还是对象权限。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

关键定义

MarginManager 的定义直接说明它为什么复杂:

public struct MarginManager<phantom BaseAsset, phantom QuoteAsset> has key {
    id: UID,
    owner: address,
    deepbook_pool: ID,
    margin_pool_id: Option<ID>,
    balance_manager: BalanceManager,
    deposit_cap: DepositCap,
    withdraw_cap: WithdrawCap,
    trade_cap: TradeCap,
    borrowed_base_shares: u64,
    borrowed_quote_shares: u64,
    take_profit_stop_loss: TakeProfitStopLoss,
    extra_fields: VecMap<String, u64>,
}

字段分三层读:

  • 账户层:ownerbalance_manager、三种 cap。
  • 市场层:deepbook_poolmargin_pool_id
  • 风险层:borrowed_base_sharesborrowed_quote_sharestake_profit_stop_loss

这不是“一个仓位对象”,而是一个能代表用户调用 DeepBook、同时被 Margin 风控约束的共享账户。

读风险控制面

MarginManager<BaseAsset, QuoteAsset> 是一个共享对象,代表用户在某个 DeepBook Pool 上的 Margin 账户。它绑定:

  • owner:只有 owner 可以存取、借款、还款、注册和注销 manager。
  • deepbook_pool:这个 manager 只能服务一个 DeepBook Pool。
  • balance_managerdeposit_capwithdraw_captrade_cap:DeepBook 账户能力,由 manager 持有。
  • margin_pool_id:当前债务来自哪个 MarginPool。源码限制一个 manager 同时只能从一个 MarginPool 借款。
  • borrowed_base_sharesborrowed_quote_shares:债务份额,不直接存金额。
  • take_profit_stop_loss:条件订单集合。

源码旁白MarginManager 是一个复合账户,不是单纯的“仓位对象”。它内部持有 DeepBook 的 BalanceManager 和三种 cap,所以 Margin 交易仍然沿用现货池的账户和权限模型,只是在外层叠加借贷和风险约束。

创建路径是 newnew_with_initializernew 创建后直接 share_objectnew_with_initializerManagerInitializer 强制调用方最后执行 share,避免 manager 被创建后没有共享。

注册路径是 register_margin_manager,它调用 MarginRegistry.add_margin_manager。注销路径是 unregister_margin_manager,源码要求 base/quote debt 都为 0,margin_pool_id 为空,base/quote/DEEP 余额也为 0。

工程旁白

MarginManager 的债务字段是 shares 而不是金额,这意味着“当前欠款”必须结合 MarginPoolmargin_state 计算。应用侧展示 manager 时至少要同时读 manager、对应 MarginPool 和 oracle 价格,否则只能看到静态份额,无法得到真实风险率。

源码限制一个 manager 同时只从一个 MarginPool 借款,目的是让清算、还款和风险率计算维持明确的 debt asset。多市场应用如果希望用户交易多个池,应按 (owner, pool) 维护多个 manager,而不是把不同池仓位塞进同一个账户。

风控判断

  • 创建 manager 后必须注册到 registry,方便应用和清算机器人枚举。
  • 注销前检查 base debt、quote debt、margin_pool_id、base/quote/DEEP 余额和 open orders 都已清空。
  • 不要在前端暴露 manager 持有的 cap;用户只签 PTB,cap 由共享对象内部使用。

动手检查

  • 该 manager 绑定的是哪个 DeepBook Pool,是否和交易 PTB 传入的 Pool 一致?
  • 当前 debt asset 是 base、quote 还是无债务,borrow shares 如何换算成 amount?
  • 注销失败时,是债务未清、余额未提、挂单未结算还是 manager 未从 registry 移除?

ch08-04 MarginPool<Asset>

返回本章

先看风险边界

这里先把问题放到风险控制面里。“MarginPool<Asset>”不是在 DeepBook 旁边加一层 UI,而是把 registry、manager、oracle、borrow/supply state 接进同一条链上风控路径。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

关键定义

MarginPool 的核心字段如下:

public struct MarginPool<phantom Asset> has key, store {
    id: UID,
    vault: Balance<Asset>,
    state: State,
    config: ProtocolConfig,
    protocol_fees: ProtocolFees,
    positions: PositionManager,
    allowed_deepbook_pools: VecSet<ID>,
    rate_limiter: RateLimiter,
    extra_fields: VecMap<String, u64>,
}

字段要按三组读:

  • 资金和份额:vaultstatepositions
  • 风险和限制:configallowed_deepbook_poolsrate_limiter
  • 收费和扩展:protocol_feesextra_fields

供应入口是公开函数,借款入口是包内函数。这一点很重要:普通用户可以 supply,但借款必须通过 MarginManager 和协议内部风控路径进入,不能绕过 manager 直接从池子借资产。

读风险控制面

MarginPool<Asset> 是一个单资产借贷池。核心字段包括:

  • vault:真实资产余额。
  • statemargin_state::State,保存 total supply、total borrow 和 shares。
  • configProtocolConfig,保存利率曲线、供应上限、最大利用率、协议 spread、最小借款和 rate limit。
  • positions:供应者 shares 表。
  • protocol_fees:维护者、协议和 referral 费用。
  • allowed_deepbook_pools:哪些 DeepBook Pool 的 manager 可以向本池借款。
  • rate_limiter:控制提款速率。

供应者需要 SupplierCapsupply 接收 Coin<Asset>SupplierCap 和 referral,调用 state.increase_supply 获得 supply shares,再把 coin 加入 vault。withdraw 用用户 shares 换算可提金额,检查 rate limit 和 vault 余额后返回 coin。

借款入口是 public(package) fun borrow,只有 Margin package 内部能调用。它检查:

  • 借款金额不低于 min_borrow
  • 借款后利用率不超过 max_utilization_rate
  • DeepBook Pool 已被允许向该 MarginPool 借款。
  • vault 中有足够资产可 split。

还款分普通还款和清算还款:repay 按 shares 减少债务,repay_liquidation 还会把奖励、坏账或额外资产反映到 supply 侧。

工程旁白

MarginPool 同时服务两类用户:供应者把资产放入 vault 获取 supply shares,交易者通过 manager 借出资产形成 borrow shares。供应者赚取的不是固定利息转账,而是 shares 对应的可提金额随 total_supply 增加。

借款失败时要把池级限制和账户级限制分开:池级包括 vault 流动性、max utilization、min borrow 和 allowed DeepBook Pool;账户级包括借款后风险率。这样 UI 才能告诉用户是“池子没钱”还是“你的抵押不够”。

风控判断

  • 供应和抵押是两个入口,供应资产不能直接提高某个 manager 的风险率。
  • 提款前读取 rate limiter 和 vault 余额,避免把 shares 可换算金额当成一定可提金额。
  • 清算还款走 repay_liquidation,它可能改变供应侧收益、奖励和坏账处理。

动手检查

  • 用户看到的 APR 来自 ProtocolConfig 和当前 utilization,还是写死的显示值?
  • withdraw 可提金额是否同时考虑 shares、vault balance 和 rate limit?
  • 借款失败时应该检查 MarginPool 限制还是 manager 风险率,二者如何排序?

ch08-05 pool_proxy 如何连接 Margin 和 DeepBook Pool

返回本章

先看风险边界

Margin 下单最危险的误解,是把它看成“普通 DeepBook 下单 + 借钱”。如果应用直接拿 BalanceManager 调现货 Pool,就会绕过 Margin 的价格保护、池启用状态和 reduce-only 规则。

pool_proxy 的存在就是为了堵住这个口子:所有 Margin 交易先过 registry 和 manager,再进入 DeepBook Pool。

源码入口

按调用顺序看这三处:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

关键定义

pool_proxy 的限价单入口把 Margin 检查放在 DeepBook 下单之前:先确认 manager 属于这个 pool,再检查 registry 是否启用,最后做 oracle 价格保护。

public fun place_limit_order<BaseAsset, QuoteAsset>(
    registry: &MarginRegistry,
    margin_manager: &mut MarginManager<BaseAsset, QuoteAsset>,
    pool: &mut Pool<BaseAsset, QuoteAsset>,
    client_order_id: u64,
    order_type: u8,
    self_matching_option: u8,
    price: u64,
    quantity: u64,
    is_bid: bool,
    pay_with_deep: bool,
    expire_timestamp: u64,
    clock: &Clock,
    ctx: &TxContext,
): OrderInfo {
    assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool);
    assert!(registry.pool_enabled(pool), EPoolNotEnabledForMarginTrading);

    registry.assert_price(pool.id(), price, is_bid, clock);

    let trade_proof = margin_manager.trade_proof(ctx);
    let balance_manager = margin_manager.balance_manager_trading_mut(ctx);

    pool.place_limit_order(
        balance_manager,
        &trade_proof,
        client_order_id,
        order_type,
        self_matching_option,
        price,
        quantity,
        is_bid,
        pay_with_deep,
        expire_timestamp,
        clock,
        ctx,
    )
}

这个代理层是 Margin 应用的关键安全边界。普通 DeepBook 下单只知道 BalanceManager 和 TradeProof;Margin 下单还必须知道账户风险、池是否启用、价格是否偏离 oracle。SDK 设计时应把这一层作为默认入口。

读风险控制面

常规 Margin 下单先做风控检查,再进入现货撮合:

  1. margin_manager.deepbook_pool() == pool.id() 防止 manager 和交易对错配。
  2. registry.pool_enabled(pool) 防止在禁用池里继续加风险。
  3. 限价单直接检查用户 price;市价单先从订单簿算 effective price,再检查 oracle 偏离。
  4. MarginManagertrade_proof 和可变 balance_manager
  5. 调用 DeepBook Pool 的 place_limit_orderplace_market_order

当某个 Pool 被禁用 Margin 交易时,用户仍可能需要减少风险。place_reduce_only_limit_orderplace_reduce_only_market_order 会读取当前债务和资产,只允许减少净负债方向的订单,否则抛出 ENotReduceOnlyOrder

工程旁白

pool_proxy 的价值不只是代调用 DeepBook。它把 registry 的启用状态、oracle 价格保护、manager 的 TradeCap 和 reduce-only 判断放在同一条链上路径里,保证下单不会绕过 Margin 风控。

市价单需要先根据订单簿推导 effective price,再与 oracle 检查;限价单则直接检查用户给出的 price。应用侧因此要把滑点、订单簿深度和 oracle stale 风险都显示在交易预览中。

风控判断

  • SDK 默认只暴露 Margin 下单入口;原始 DeepBook Pool 下单只能给专业或内部工具使用。
  • Pool 被禁用时,界面不要简单禁用全部按钮,应保留 reduce-only 平仓或降风险路径。
  • 错误提示要区分 oracle 价格保护、订单簿流动性不足、pool 未启用和 manager-pool 错配。

动手检查

  • 这笔订单会增加债务风险,还是符合 reduce-only 的降风险方向?
  • 市价单的 effective price 是否经过 registry.assert_price 检查?
  • 应用是否在下单后调用 withdraw settled amounts 或重新读取结算余额?

ch08-06 风险率

返回本章

先看风险边界

这里先把问题放到风险控制面里。“风险率”不是在 DeepBook 旁边加一层 UI,而是把 registry、manager、oracle、borrow/supply state 接进同一条链上风控路径。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

关键定义

风险阈值来自 PoolConfigRiskRatios

public struct PoolConfig has copy, drop, store {
    base_margin_pool_id: ID,
    quote_margin_pool_id: ID,
    risk_ratios: RiskRatios,
    user_liquidation_reward: u64,
    pool_liquidation_reward: u64,
    enabled: bool,
    extra_fields: VecMap<String, u64>,
}

public struct RiskRatios has copy, drop, store {
    min_withdraw_risk_ratio: u64,
    min_borrow_risk_ratio: u64,
    liquidation_risk_ratio: u64,
    target_liquidation_risk_ratio: u64,
}

这不是前端配置项,而是链上风控参数。enabled 控制某个 DeepBook pool 是否允许 Margin 交易;base_margin_pool_idquote_margin_pool_id 指向可借资产池;四个 risk ratio 决定借款、提款、清算触发和清算目标。UI 中如果只显示一个“健康度”,会掩盖这些动作边界。

读风险控制面

风险率在 margin_manager.moverisk_ratiorisk_ratio_unsaferisk_ratio_int 中计算。直观公式是:

risk_ratio = assets_in_debt_unit / debt

如果 debt 是 base,则把 quote 资产按 oracle 价格换算成 base;如果 debt 是 quote,则把 base 资产换算成 quote。assets_in_debt_unit 会包含 manager 中可用资产、结算后资产和订单簿中相关余额。

四个阈值决定动作边界:

  • min_borrow_risk_ratio:借款后必须高于该值,否则 borrow_base / borrow_quoteEBorrowRiskRatioExceeded
  • min_withdraw_risk_ratio:提款后必须高于该值,否则 withdrawEWithdrawRiskRatioExceeded
  • liquidation_risk_ratio:低于该值后 can_liquidate 为真。
  • target_liquidation_risk_ratio:清算时用于计算最多应还多少债,目标是把剩余仓位拉回目标风险率附近。

生产应用不要只用前端本地价格判断风险率。交易前必须用当前 Pyth price object 做 dry run 或 dev inspect,因为链上还会检查 stale price、置信区间和 EWMA 偏差。

工程旁白

风险率计算要先确定 debt asset。借 quote 开多时,base 资产需要按价格换算成 quote;借 base 开空时,quote 资产需要换算成 base。价格方向搞反会直接导致健康度展示错误。

风险率不是只看 BalanceManager 可用余额,还要考虑 DeepBook Pool 中已结算但未提取的金额、订单相关余额以及随时间增长的 debt amount。清算机器人应使用链上读接口或 dev inspect,避免 indexer 延迟造成误判。

风控判断

  • 显示风险率时同时展示 debt asset、债务金额、阈值和价格时间戳。
  • 借款、提款、交易、TPSL 创建前都要模拟操作后的风险率。
  • 把风险率小于 1、低于清算阈值、低于提款阈值分别映射成不同 UI 状态。

动手检查

  • 当前风险率是用 base 计价还是 quote 计价?
  • 哪些资产余额被计入 assets_in_debt_unit,open orders 是否已处理?
  • 价格过期或 confidence 过大时,应用是阻止交易还是只给提示?

ch08-07 position_manager

返回本章

先看风险边界

读“position_manager”时先问:这一步会让账户风险变大还是变小,谁有权继续执行,失败时应该归因到价格、债务、池配置还是对象权限。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

读风险控制面

position_manager.move 不管理交易仓位,它管理供应者在 MarginPool 中的存款份额。Position 只有两个核心字段:

  • shares:用户供应 shares。
  • referral:供应 referral ID。

increase_user_supply 会在首次供应时添加 entry,然后增加 shares,并返回新的总 shares 和上一次 referral。decrease_user_supply 减少 shares,user_supply_shares 是读接口。

这个模块的设计重点是把“供应者权益”从 vault 金额中抽离。利息发生后,用户 shares 不变,shares 对应的资产金额通过 margin_state.supply_shares_to_amount 动态计算。

工程旁白

这个模块容易被误读为“仓位管理器”。实际上交易仓位在 MarginManager 和 DeepBook orders 中体现;position_manager 只记录谁给借贷池供应了多少 shares,以及 referral 归属。

供应者收益来自 shares 对应资产金额的增长,所以 indexer 不应只保存最初 deposit amount。展示供应仓位时要用最新 pool state 把 shares 换回 amount,并扣除提款限速和 vault 流动性的影响。

风控判断

  • 供应仓位页命名为 lending position 或 supply position,避免与 margin trading position 混淆。
  • SupplierCap 作为供应者操作凭证,不能用 manager owner 代替。
  • referral 变更要按源码返回的 previous referral 处理事件和统计。

动手检查

  • 这个 position 是借贷池供应仓位还是杠杆交易仓位?
  • 用户 shares 不变时,为什么可提 amount 会随利息变化?
  • 提款失败时是 shares 不足、rate limit 还是 vault liquidity 不足?

ch08-08 margin_state 与利息累计

返回本章

先看风险边界

这里先把问题放到风险控制面里。“margin_state 与利息累计”不是在 DeepBook 旁边加一层 UI,而是把 registry、manager、oracle、borrow/supply state 接进同一条链上风控路径。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

关键定义

State 用 amount 和 shares 分离“池总量”和“用户份额”。利息改变 total_supply / total_borrow,但不会直接改变每个用户持有的 shares。

public struct State has drop, store {
    total_supply: u64,
    total_borrow: u64,
    supply_shares: u64,
    borrow_shares: u64,
    last_update_timestamp: u64,
    extra_fields: VecMap<String, u64>,
}

public(package) fun increase_supply(
    self: &mut State,
    config: &ProtocolConfig,
    amount: u64,
    clock: &Clock,
): (u64, u64) {
    let protocol_fees = self.update(config, clock);
    let ratio = self.supply_ratio();
    let shares = math::div(amount, ratio);
    self.supply_shares = self.supply_shares + shares;
    self.total_supply = self.total_supply + amount;

    (shares, protocol_fees)
}

供应 shares 使用向下取整,借款 shares 在增加债务时使用向上取整,这是借贷协议里常见的保守精度处理:不能让借款人因为整数舍入少承担债务。读 MarginPool 代码时,先找每个入口是否调用 update,再看 shares 和 amount 的换算方向。

读风险控制面

margin_state::State 保存:

  • total_supply
  • total_borrow
  • supply_shares
  • borrow_shares
  • last_update_timestamp

每次 increase_supplydecrease_supply_sharesincrease_borrowdecrease_borrow_shares 都先调用 updateupdate 根据当前利用率和经过时间计算利息:

utilization_rate = total_borrow / total_supply
interest_rate = piecewise_rate(utilization_rate)
interest = total_borrow * interest_rate * elapsed_ms / year_ms
protocol_fees = interest * protocol_spread
total_supply += interest - protocol_fees
total_borrow += interest

supply shares 使用 math::div(amount, supply_ratio),borrow shares 使用 math::div_round_up(amount, borrow_ratio)。借款 round up 是为了避免借款人用精度误差少记债务。

工程旁白

利息累计是懒更新模型:只有供应、提款、借款、还款等操作进入 update 时,才按 last_update_timestamp 到当前时间计算一次。用户 shares 静止,池的 total_borrowtotal_supply 改变,导致 shares 换算出来的 amount 改变。

协议费用来自利息而不是本金。protocol_spread 从借款人产生的 interest 中划出一部分进入 fee 逻辑,其余体现在供应侧收益中。应用估算 APR 时应基于 utilization 和利率曲线,而不是把过去收益线性外推。

风控判断

  • 每次展示 debt amount 前用最新 state 换算 borrow shares,不要缓存借款时 amount。
  • 模拟还款时注意 round up/round down,避免还款金额换算成 0 shares。
  • 利率图要标出 optimal utilization,因为超过后 excess slope 会显著抬高借款成本。

动手检查

  • 多久没有触发 update,这会如何影响下一次操作中的利息跳变?
  • borrow shares 和 supply shares 分别在什么场景增加或减少?
  • protocol fees 从哪部分利息中扣除,是否影响供应者收益展示?

ch08-09 protocol_config

返回本章

先看风险边界

读“protocol_config”时先问:这一步会让账户风险变大还是变小,谁有权继续执行,失败时应该归因到价格、债务、池配置还是对象权限。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

关键定义

ProtocolConfig 把池级限制和利率曲线分成两组。这样维护者可以更新供给上限、提款限速,也可以单独调整利用率驱动的利率参数。

public struct ProtocolConfig has copy, drop, store {
    margin_pool_config: MarginPoolConfig,
    interest_config: InterestConfig,
    extra_fields: VecMap<String, u64>,
}

public struct MarginPoolConfig has copy, drop, store {
    supply_cap: u64,
    max_utilization_rate: u64,
    protocol_spread: u64,
    min_borrow: u64,
    rate_limit_capacity: u64,
    rate_limit_refill_rate_per_ms: u64,
    rate_limit_enabled: bool,
}

public struct InterestConfig has copy, drop, store {
    base_rate: u64,
    base_slope: u64,
    optimal_utilization: u64,
    excess_slope: u64,
}

max_utilization_rateoptimal_utilization 不解决同一个问题。前者是风控硬限制,后者是利率曲线的拐点。应用计算“还能借多少”时要用硬限制;展示 APR 变化时才使用利率曲线。

读风险控制面

protocol_config.move 定义两组配置:

  • MarginPoolConfigsupply_capmax_utilization_rateprotocol_spreadmin_borrowrate_limit_capacityrate_limit_refill_rate_per_msrate_limit_enabled
  • InterestConfigbase_ratebase_slopeoptimal_utilizationexcess_slope

利率曲线是两段式:

u < optimal:  base_rate + u * base_slope
u >= optimal: base_rate + optimal * base_slope + (u - optimal) * excess_slope

部署脚本 scripts/transactions/updateInterestRates.ts 展示了管理员如何更新利率参数和池配置。例如 DEEP 池配置包含 supplyCapmaxUtilizationRatereferralSpreadminBorrowrateLimitCapacityrateLimitRefillRatePerMs

工程旁白

两段式利率曲线让资金利用率接近或超过 optimal 时借款成本快速上升。应用侧的“可借额度”不应只展示当前 vault 余额,还要根据 max utilization 和用户风险率计算真实上限。

rate limit 保护提款速度,避免供应者在极端行情中瞬间抽干 vault。即便 supply shares 能换算出较大 amount,提款交易仍可能因为 capacity 或 refill 不足失败。

风控判断

  • 配置面板要把池级风险参数与交易对级 PoolConfig 分开展示。
  • 更新利率脚本属于维护者/管理员操作,普通用户交易流程不需要这些 cap。
  • 可借额度计算同时取 vault balance、max utilization、min borrow 和用户风险约束的交集。

动手检查

  • 当前 utilization 位于 optimal 左侧还是右侧,借款 APR 对新增借款有多敏感?
  • 用户提款失败是 supply cap、rate limit 还是 vault 余额问题?
  • min_borrow 对小额测试交易和全额还款有什么边界影响?

ch08-10 protocol_fees

返回本章

先看风险边界

这里先把问题放到风险控制面里。“protocol_fees”不是在 DeepBook 旁边加一层 UI,而是把 registry、manager、oracle、borrow/supply state 接进同一条链上风控路径。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

读风险控制面

protocol_fees.move 把利息中的协议 spread 拆成三类账:

  • maintainer fees:维护者可通过 withdraw_maintainer_fees 提取。
  • protocol fees:协议管理员可通过 withdraw_protocol_fees 提取。
  • referral fees:供应 referral 按 shares 追踪。

MarginPool.supplywithdraw 会根据用户 referral 调整 ProtocolFees 内部 shares。margin_state.update 返回本次累计的 protocol fees,调用方再把它记入费用模块。阅读这部分时要区分两种 shares:借贷池的 supply/borrow shares 表示本金权益,费用模块的 shares 表示 referral 分配权重。

工程旁白

费用模块把“利息增长”和“费用领取”解耦。借款人看到 debt 增长,供应者看到 shares 对应 amount 增长,协议和维护者则通过 fee 状态累积可领取金额。

清算奖励不应计入普通借款 APR。清算时的 user liquidation reward 与 pool liquidation reward 来自 PoolConfig,它们影响清算 payout 和坏账覆盖,不是每秒 accruing 的利息费用。

风控判断

  • 收益页把 borrow APR、supply APR、protocol fee 和 liquidation reward 分栏展示。
  • 统计 referral 收益时读取供应 position 中的 referral,而不是交易 manager。
  • 清算事件单独入账,避免把清算奖励误算为常规利息。

动手检查

  • 本次费用来自持续借款利息,还是来自清算流程?
  • protocol spread 变化会影响供应者 APR、借款人 APR 还是两者展示?
  • referral 费用应按 supply position 还是 manager owner 归属?

ch08-11 oracle 与 stale price 风险

返回本章

先看风险边界

读“oracle 与 stale price 风险”时先问:这一步会让账户风险变大还是变小,谁有权继续执行,失败时应该归因到价格、债务、池配置还是对象权限。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

读风险控制面

helper/oracle.move 从 Pyth PriceInfoObject 读取价格,并检查:

  • price feed ID 是否匹配资产类型。
  • price 是否有效。
  • confidence 是否超过 max_conf_bps
  • EWMA 价格偏差是否超过 max_ewma_difference_bps
  • price age 是否超过配置的 max age。

pool_proxy.update_current_price 会计算安全价格并写入 MarginRegistry,下单时 assert_price 用这个价格做偏离保护。margin_manager.risk_ratio_unsafemanager_state 可用于读取展示,但开发者不能把 unsafe 结果当成链上执行一定成功的依据。

工程旁白

oracle 不是只服务 UI 估值,它是链上执行条件。registry.assert_price 会在交易入口检查价格是否可接受,risk_ratio 也要用价格把 base/quote 资产折算到 debt unit。

stale price 风险会同时影响误放大仓位和误清算。应用可以展示本地估值,但提交交易前必须通过 dry run 或 dev inspect 使用当前 price object,让链上 freshness 与 confidence 检查给出最终结果。

风控判断

  • 价格组件展示 publish time、confidence 状态和与订单价格的偏差。
  • 当 oracle stale 时禁用新增借款和开仓,但可考虑保留还款、补抵押等降风险操作。
  • 不要用 indexer 缓存价格代替 PTB 中传入的 Pyth price object。

动手检查

  • 交易失败是订单价格偏离 oracle,还是 oracle 本身 stale/confidence 不合格?
  • 风险率计算是否使用了和链上同方向的 base/quote 换算?
  • oracle 不可用时哪些用户操作仍应允许?

ch08-12 TPSL 条件订单

返回本章

先看风险边界

这里先把问题放到风险控制面里。“TPSL 条件订单”不是在 DeepBook 旁边加一层 UI,而是把 registry、manager、oracle、borrow/supply state 接进同一条链上风控路径。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

读风险控制面

tpsl.move 保存两组有序条件订单:

  • trigger_below:价格低于触发价时执行,常用于止损。
  • trigger_above:价格高于触发价时执行,常用于止盈。

Condition 包含 trigger_below_pricetrigger_pricePendingOrder 支持限价单和市价单字段:client_order_idorder_typeself_matching_optionpricequantityis_bidpay_with_deepexpire_timestamp

用户通过 MarginManager.add_conditional_order 新增 TPSL,通过 cancel_conditional_ordercancel_all_conditional_orders 取消。execute_conditional_orders 是 permissionless,任何人都可以触发;执行时会重新计算当前价格,收集可执行、过期、资金不足和价格越界的订单,再发出对应事件。

工程旁白

TPSL 只保存条件和待执行订单参数,真正成交仍要走 pool_proxy。因此触发时仍可能因为资金不足、价格越界、Pool 暂停或订单簿流动性不足而失败,失败事件必须进入 UI。

止损单的产品语义通常是降低风险,但如果参数方向错误也可能扩大债务。应用创建 TPSL 前应根据 debt asset、is_bid 和 quantity 判断是否 reduce-only,并提示触发后预估风险率。

风控判断

  • 后台 worker 可以 permissionless 执行,但不能假设每个触发订单都会成交。
  • 同一 manager 的 TPSL 列表要有用户维度唯一 ID,方便取消和事件对账。
  • 触发价来自 oracle 检查,不等同于 DeepBook 最终成交价。

动手检查

  • 这个 TPSL 触发后会买入还是卖出,是否降低当前 debt asset 风险?
  • 执行失败事件应该如何反馈给用户和重试 worker?
  • max_orders_to_execute 如何设置才能控制 gas 和延迟?

ch08-13 清算与 liquidation vault

返回本章

先看风险边界

读“清算与 liquidation vault”时先问:这一步会让账户风险变大还是变小,谁有权继续执行,失败时应该归因到价格、债务、池配置还是对象权限。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

读风险控制面

margin_manager::liquidate 是清算核心入口。流程是:

  1. 校验 manager 属于对应 DeepBook Pool,债务来自传入的 MarginPool。
  2. 计算风险率,要求 registry.can_liquidate(pool.id(), risk_ratio)
  3. 要求还款 coin 不低于 min_liquidation_repay
  4. 提取已结算金额并取消所有挂单。
  5. 根据 debt、assets、用户奖励、池奖励和目标风险率计算 repay_amount
  6. 调用 MarginPool.repay_liquidation 降低 borrow shares。
  7. 从 manager 中取出清算者应得资产,发出 LiquidationEvent

liquidation_vault.move 是清算机器人常用的资金库。管理员用 deposit<T> 注入资产,用 authorize_trader 授权交易员。交易员调用 liquidate_baseliquidate_quote,vault 先取出债务资产,调用 margin_manager.liquidate,再把收到的 base、quote 和剩余 repay asset 存回 vault。

工程旁白

清算不是简单“拿全部抵押还全部债”。源码会根据当前风险率、目标风险率、债务资产、可用 collateral 和奖励参数计算应还金额;如果资产不足,可能出现 pool default 或坏账处理。

liquidation_vault.move 把清算资金集中到 vault,授权 trader 调用 liquidate_base/liquidate_quote,成功后把清算所得回收到 vault。机器人因此要同时扫描不健康 manager 和 vault 库存,而不是只看风险率。

风控判断

  • 清算前用 dev inspect 估算 repay amount、奖励和清算后风险率。
  • vault 需要按 debt asset 准备库存;base 债务和 quote 债务调用不同入口。
  • 清算事件要记录 manager、pool、debt asset、repay amount、reward 和 default 情况。

动手检查

  • 当前仓位低于 liquidation_risk_ratio 还是只是低于借款/提款阈值?
  • 清算机器人是否有足够 debt asset,还是需要先 rebalance vault?
  • 清算后目标是完全平仓还是回到 target_liquidation_risk_ratio 附近?

ch08-14 正常路径与失败边界

返回本章

先看风险边界

这里先把问题放到风险控制面里。“正常路径与失败边界”不是在 DeepBook 旁边加一层 UI,而是把 registry、manager、oracle、borrow/supply state 接进同一条链上风控路径。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

读风险控制面

正常借款路径:deposit 抵押品,borrow_baseborrow_quote 从对应 MarginPool 借款,借款 coin 存入 manager,然后立即检查 min_borrow_risk_ratio

正常还款路径:用户先确保 manager 中有债务资产,调用 repay_baserepay_quote。内部按 amount 和当前 debt 计算 repay shares,从 BalanceManager 取 coin,调用 MarginPool.repay,债务清零后 margin_pool_id 变为 none。

常见失败边界:

  • Pool 未启用 Margin:EPoolNotEnabledForMarginTrading
  • 借贷池未允许该 DeepBook Pool 借款:EDeepbookPoolNotAllowedForLoan
  • 同一个 manager 已有另一借贷池债务:ECannotHaveLoanInMoreThanOneMarginPool
  • 借款后风险率不足:EBorrowRiskRatioExceeded
  • 提款后风险率不足:EWithdrawRiskRatioExceeded
  • 清算风险率尚未跌破阈值:ECannotLiquidate
  • oracle stale、confidence 过高或 EWMA 偏离过大:oracle 模块抛错。

工程旁白

正常路径的关键是对象一致性:registry 中的 PoolConfig、manager 绑定的 DeepBook Pool、MarginPool 授权列表、oracle price object 和 PTB type arguments 必须互相匹配。任何一个错位都可能表现为 Move abort。

失败边界应转成产品语言。风险率不足提示用户补抵押或降低借款;oracle stale 提示等待价格更新;reduce-only 失败提示订单方向会扩大仓位;min borrow/min repay 提示用户调整金额。

风控判断

  • 为每个写操作维护 preflight checklist,而不是只在 catch 里展示原始 abort code。
  • 把权限、余额、风险率、价格和配置错误分组,便于客服和日志分析。
  • 复杂 PTB 先 dev inspect,再 dry run,再提交,尤其是借款加交易的组合操作。

动手检查

  • 这次失败属于对象配置错误、用户余额错误、风险率错误还是 oracle 错误?
  • 如果交易部分成功不可能发生,PTB 原子性是否已向用户解释清楚?
  • 错误提示是否给出用户下一步,而不只是 Move abort 名称?

ch08-15 开多和开空资金路径

返回本章

先看风险边界

读“开多和开空资金路径”时先问:这一步会让账户风险变大还是变小,谁有权继续执行,失败时应该归因到价格、债务、池配置还是对象权限。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

读风险控制面

开多通常是“抵押 quote,借 quote,买 base”。路径是:用户存入 USDC 到 MarginManager<SUI, USDC>,调用 borrow_quoteMarginPool<USDC> 借 USDC,借款进入 manager,然后通过 pool_proxy.place_market_order 买入 SUI。此时资产端有 SUI 和可能剩余 USDC,债务端是 USDC borrow shares。

开空通常是“抵押 quote 或 base,借 base,卖 base”。路径是:用户调用 borrow_baseMarginPool<SUI> 借 SUI,借款进入 manager,然后通过 pool_proxy.place_market_order 卖出 SUI 得到 USDC。此时资产端主要是 USDC,债务端是 SUI borrow shares。

两条路径都依赖同一个风险率公式,只是 debt unit 不同。开多的风险通常对 base 下跌敏感;开空的风险通常对 base 上涨敏感。

工程旁白

开多通常借 quote,例如借 USDC 买 SUI;债务是 quote,资产侧多了 base,base 下跌会让资产折算价值下降。开空通常借 base,例如借 SUI 卖成 USDC;债务是 base,base 上涨会让用 quote 折算后的偿债压力上升。

无论开多还是开空,借来的 coin 会进入同一个 MarginManager 的 DeepBook BalanceManager,再由 pool_proxy 下单成交。它不是闪电贷,因为债务不会在同一笔交易末尾自动关闭,后续还款需要显式调用 repay_baserepay_quote

风控判断

  • 交易预览必须显示 debt asset,否则用户无法理解价格方向对风险率的影响。
  • 开仓 PTB 完成后立即结算或刷新 settled balances,避免 UI 漏算资产。
  • 组合借款加下单时,先检查借款后风险率,再检查成交后风险率。

动手检查

  • 借 quote 买 base 后,哪种价格变化会降低风险率?
  • 借 base 卖出后,还款需要回购哪种资产?
  • 如果只完成借款不下单,manager 的风险率和余额会如何显示?

ch08-16 风险率跌破阈值后的处理流程

返回本章

先看风险边界

这里先把问题放到风险控制面里。“风险率跌破阈值后的处理流程”不是在 DeepBook 旁边加一层 UI,而是把 registry、manager、oracle、borrow/supply state 接进同一条链上风控路径。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

读风险控制面

当风险率低于 liquidation_risk_ratio

  1. 任何人都可以尝试清算,清算 vault 还要求调用者在授权 trader 集合中。
  2. 清算入口先取消挂单并结算,避免订单继续改变仓位。
  3. 协议计算把风险率拉回 target_liquidation_risk_ratio 所需的还款额。
  4. 如果资产不足,可能发生 full liquidation,pool_default 记录借贷池坏账。
  5. 清算事件记录剩余资产、剩余债务、风险率和预言机价格,应用端应把它作为仓位状态同步的权威信号。

工程旁白

风险率下降不一定立刻可清算。低于 min_withdraw_risk_ratio 时应禁止提款;低于 min_borrow_risk_ratio 时应禁止新增借款;只有低于 liquidation_risk_ratio 才进入清算条件。

用户侧优先提供降风险动作:补充抵押、还款、取消挂单并结算、reduce-only 反向交易。机器人侧只在满足清算阈值且 vault 有对应 debt asset 时提交清算。

风控判断

  • UI 用同一条风险率刻度展示四个阈值,并把可用操作随状态切换。
  • Pool 暂停时仍优先保留 reduce-only、还款和补抵押路径。
  • 清算告警要包含价格来源时间,避免用 stale indexer 数据误触发。

动手检查

  • 当前状态只是禁止提款/借款,还是已经达到 can_liquidate
  • 用户最小代价的降风险动作是什么:补抵押、还款还是反向交易?
  • 清算机器人是否在提交前重新 dev inspect 风险率?

ch09 DeepBook Margin 应用开发

本章目标

  • 把 ch08 的协议结构落到 TypeScript SDK、PTB 构造、交易前检查和 UI 风控展示。
  • 能创建或加载 MarginManager,完成抵押、供应、借款、交易、TPSL、还款和平仓流程。
  • 能设计清算扫描器和 Margin 风控面板,正确展示健康度、风险率、利息和可借额度。

本章学习阶梯

  • L1 先从用户动作理解 Margin 应用:抵押、借款、交易、还款、清算。
  • L2 用 SDK/PTB 封装 manager 创建、抵押、借款和杠杆交易。
  • L3 把 UI 风险提示映射回 Move assert、oracle 和风险参数。
  • L4 做出清算机器人或 Margin 风控面板。

源码地图

小节目录

本章代码

  • code/s01-create-margin-manager/:创建或加载 MarginManager 的 CLI 骨架。
  • code/s02-deposit-collateral/:存入抵押品并做 dry run。
  • code/s03-borrow-and-trade/:借款并通过 pool_proxy 下单。
  • code/s04-repay-and-close/:还款、取消订单、结算和平仓流程。
  • code/s05-liquidation-bot/:清算扫描器骨架,连接 risk ratio、vault 余额和清算入口。

Move 高阶穿插点

  • Margin 应用开发要把用户动作拆成链上对象变化,而不是只按页面按钮设计流程。
  • 每个风险提示都应该能追溯到 Move 里的某个 assert、配置字段或 oracle 条件。
  • 清算机器人要同时懂 Move 入口、Indexer 延迟、oracle 新鲜度和 vault 资产可用性。

常见错误

  • 在用户前端暴露 admin cap 或 maintainer cap。普通用户不需要这些 cap。
  • supplyToMarginPool 当成保证金充值。它是借贷池供应,不是 manager collateral。
  • 只查 manager 余额,不查 open order 和 settled amounts。风险率可能与 UI 显示余额不一致。
  • 不做 dry run,直接提交复杂 PTB。Margin 交易依赖多个 shared object、oracle 和风险参数,失败面很多。
  • 忽略 Option<u64> 语义。还款和平仓中 none 与 some 的行为不同。
  • 清算机器人只看风险率,不看 vault 是否有对应 debt asset。

本章检查清单

  • 能区分用户配置、管理员配置和维护者配置。
  • 能创建或加载某个 (owner, poolKey)MarginManager
  • 能解释抵押、供应、借款三种资金动作的区别。
  • 能用 SDK 构造借 quote 后买 base 的 PTB。
  • 能用 SDK 构造借 base 后卖 base 的 PTB。
  • 能处理 TPSL 的创建、触发、取消和失败事件。
  • 能设计一个清算扫描器,并说明何时调用 liquidate_baseliquidate_quote

进阶练习

  • s03-borrow-and-trade 增加 --dry-run-only 参数,输出风险率变化但不提交交易。
  • s04-repay-and-close 增加 --reduce-only 模式,在 Pool 被禁用时仍允许用户降风险。
  • s05-liquidation-bot 增加 vault 资产再平衡提示:当 USDC 不足但 SUI 充足时,生成 swap_base_to_quote 交易建议。
  • 为 UI 错误提示写一张 abort code 映射表,并把每个错误绑定到用户可执行的下一步。

ch09-01 Margin 交易应用模块设计

返回本章

先看用户路径

这一节从 Margin 用户动作读起:围绕“Margin 交易应用模块设计”,先把 manager、collateral、loan、trade 和 repay 的顺序排清楚,再看源码入口。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

一个可用的 Margin 应用至少拆成六个模块:

  • 配置模块:网络、DeepBook package、Margin package、registry、Pyth price object、coin type、Pool key、MarginPool ID。
  • 账户模块:查询、创建、注册、注销 MarginManager,维护 owner 到 manager ID 的映射。
  • 资金模块:抵押品存入和提取,供应资产到 MarginPool,展示 vault liquidity 和用户 supply shares。
  • 交易模块:借入 base/quote,调用 pool_proxy 下限价单、市价单、撤单和结算。
  • 风控模块:风险率、健康度、利息、可借额度、可提额度、TPSL 状态和清算阈值。
  • 机器人模块:TPSL 触发、清算扫描、liquidation vault 余额和授权 trader 管理。

后端不要把“估算成功”当成“交易一定成功”。每个写交易都要执行 dry run 或 dev inspect,并把 Move abort code 映射成用户能理解的错误。

工程旁白

应用架构的关键是把“配置真值”和“用户会话状态”分开。registry、PoolConfig、MarginPool 和 oracle object 是链上真值;本地 SDK 配置只是帮助构造 PTB;Indexer 缓存只用于查询加速。

交易模块不要暴露 DeepBook 原始下单作为 Margin 下单的快捷方式。Margin 持续债务必须通过 manager 和 pool_proxy 进入现货池;DeepBookV3 闪电贷如果用于其他场景,也应作为独立功能,不和 margin borrow/repay 共享状态。

Margin 应用判断

  • 每个写操作都生成 PTB、dry run、错误映射和事件刷新四个步骤。
  • 账户模块按 (owner, poolKey) 组织 manager,避免多池仓位混淆。
  • 机器人模块需要独立密钥和权限,不能复用用户签名或 admin cap。

动手检查

  • 这个功能读的是链上真值、Indexer 派生状态还是前端缓存?
  • 用户操作会产生持续债务吗,是否需要展示 interest accrual?
  • 机器人执行 TPSL/清算时,失败事件如何回流到 UI?

ch09-02 初始化 Margin SDK 配置

返回本章

先看用户路径

这里先从用户动作往回看。“初始化 Margin SDK 配置”在应用里通常是一组 PTB 和状态刷新,不是一条孤立 move call;每一步都要同时考虑余额、债务和风险率。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

脚本使用 @mysten/sui@mysten/deepbook-v3

import { SuiGrpcClient } from "@mysten/sui/grpc";
import { Transaction } from "@mysten/sui/transactions";
import { deepbook } from "@mysten/deepbook-v3";

const env = "mainnet";
const client = new SuiGrpcClient({
  baseUrl: "https://sui-mainnet.mystenlabs.com",
  network: "mainnet",
}).$extend(
  deepbook({
    address: adminCapOwner[env],
    adminCap: adminCapID[env],
    marginAdminCap: marginAdminCapID[env],
    marginMaintainerCap: marginMaintainerCapID[env],
  }),
);

const tx = new Transaction();

普通用户操作通常只需要 address。管理员和维护者脚本才传 adminCapmarginAdminCapmarginMaintainerCapmarginPrep.ts 是管理员视角,不要把其中 cap 配置复制到用户前端。

生产配置建议用一份 network config:

  • network 和 RPC endpoint。
  • DeepBook package 和 Margin package 版本。
  • MarginRegistry shared object。
  • 支持的 pool key,如 SUI_USDCDEEP_USDC
  • base/quote Pyth price object。
  • base/quote MarginPool ID。
  • liquidation vault ID。

工程旁白

初始化脚本展示的是管理员流程,普通应用只需要读取结果并构造用户 PTB。不要把 AdminCap、maintainer cap 或 pause cap 放进前端配置;前端配置应只包含 package ID、registry ID、DeepBook Pool ID、MarginPool ID、Pyth price object 和 type arguments。

不同网络的对象 ID 可能完全不同。SDK 配置要支持按 network 加载,并在启动时用链上读取校验 registry 版本、pool enabled、loan enabled 和 price feed 是否匹配,避免用户签名后才发现配置错。

Margin 应用判断

  • 把 admin 初始化配置和用户交易配置分文件管理。
  • 启动时校验 package version、registry allowed version 和 PoolConfig。
  • price object 配置要和 base/quote 类型绑定,不能只按 symbol 字符串匹配。

动手检查

  • 当前配置是否包含任何不该给前端的 cap?
  • DeepBook Pool、base MarginPool、quote MarginPool 和 Pyth price object 是否来自同一网络?
  • SDK 在构造 PTB 前是否验证 registry 中的 pool enabled 状态?

ch09-03 创建或加载 MarginManager

返回本章

先看用户路径

这一节从 Margin 用户动作读起:围绕“创建或加载 MarginManager”,先把 manager、collateral、loan、trade 和 repay 的顺序排清楚,再看源码入口。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

创建 manager 对应链上 margin_manager.movenewnew_with_initializer。SDK 通常会封装成类似 client.deepbook.marginManager.createMarginManager(poolKey) 的交易构造器,最终需要传 DeepBook Pool、DeepBook Registry、MarginRegistry、Clock 和 owner。

加载 manager 的推荐顺序:

  1. 先从本地缓存或后端数据库按 (owner, poolKey) 查 manager ID。
  2. 如果没有,查询 MarginRegistry.margin_managers 对应的事件或对象索引。
  3. 如果仍不存在,引导用户创建。
  4. 创建成功后保存 MarginManagerCreatedEvent 中的 margin_manager_idbalance_manager_id

创建后通常要注册到 registry,便于应用按用户地址枚举 manager。注销前必须检查:

  • 没有 base debt。
  • 没有 quote debt。
  • margin_pool_id 为空。
  • base、quote、DEEP 余额都为 0。

工程旁白

应用首次进入某个交易对时,应先从 registry 或 indexer 查 (owner, pool) 是否已有 manager。没有时再构造创建交易;有多个 manager 时要提示用户选择或按业务规则固定使用一个。

创建 manager 是账户初始化,不等于存入保证金或借款。创建成功后风险率通常仍为空或无债务状态,UI 应引导用户下一步存入抵押品,而不是展示可交易杠杆额度。

Margin 应用判断

  • manager ID 绑定 pool key 存储,切换交易对时重新加载。
  • 创建后等待 shared object 可读再允许后续 deposit/borrow。
  • 注销入口只在无债务、无余额、无挂单和无 TPSL 时显示。

动手检查

  • 当前 owner 在这个 pool 是否已经有 manager?
  • 创建交易是否传入正确的 base/quote type arguments 和 DeepBook Pool ID?
  • 加载到的 manager 是否已经注册到 MarginRegistry

ch09-04 存入和提取抵押品

返回本章

先看用户路径

这里先从用户动作往回看。“存入和提取抵押品”在应用里通常是一组 PTB 和状态刷新,不是一条孤立 move call;每一步都要同时考虑余额、债务和风险率。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

抵押品存入调用 MarginManager.deposit<BaseAsset, QuoteAsset, DepositAsset>DepositAsset 必须是 base、quote 或 DEEP,否则 deposit_int 会抛 EInvalidDeposit

应用流程:

  1. 查询用户 coin objects,并合并或选择足够余额。
  2. 构造 PTB,把 coin 传给 manager deposit。
  3. 传入 base 和 quote Pyth price object,链上会在 base/quote 抵押存入时记录价格事件。
  4. 执行前 dry run,捕获无效 coin、owner 不匹配、price object 错误等问题。

提取调用 withdraw,会先从 BalanceManager 取出 coin,再重新计算风险率。若 manager 有债务且提款后低于 min_withdraw_risk_ratio,交易会 abort,状态回滚。

UI 应提供“最大可提”而不是只显示余额。最大可提要考虑当前债务、价格、open order、min_withdraw_risk_ratio 和 stale price 风险。

工程旁白

存入抵押品会增加 manager 内 BalanceManager 的资产余额,可能提高风险率;供应到 MarginPool 则把资产放入借贷池 vault,换取 supply shares,不会直接改善某个 manager 的健康度。

提款是高风险操作,因为它会降低资产侧价值。应用在允许提款前要用最新 price object 模拟 withdraw 后的风险率,并检查 min_withdraw_risk_ratio,否则用户会在签名后遇到 abort。

Margin 应用判断

  • 充值页面明确标注“存入保证金”和“供应赚息”两个不同入口。
  • 提款金额默认给出安全上限,而不是全部 BalanceManager 余额。
  • DEEP 可作为协议相关资产展示,但仍要遵守 EInvalidDeposit 的资产类型限制。

动手检查

  • 这次资金进入 manager 还是 MarginPool vault?
  • 提款后风险率是否仍高于 min_withdraw_risk_ratio
  • 用户要提取的是可用余额,还是仍锁在 open orders 或未结算金额中?

ch09-05 供应资产到 MarginPool

返回本章

先看用户路径

这一节从 Margin 用户动作读起:围绕“供应资产到 MarginPool”,先把 manager、collateral、loan、trade 和 repay 的顺序排清楚,再看源码入口。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

供应资产是成为借贷池 LP,不是给自己的 manager 加保证金。参考 scripts/transactions/supplyToMarginPool.ts

client.deepbook.marginPool.supplyToMarginPool(
  "USDC",
  tx.object(supplierCapID[env]),
  90_000,
)(tx);

链上 MarginPool.supply 会:

  • state.increase_supply 更新利息。
  • 增加供应者 supply shares。
  • 更新 referral fee shares。
  • 把 coin 加入 vault。
  • 记录 rate limiter deposit。
  • 检查 total_supply <= supply_cap

供应页面需要展示:

  • 用户 supply amount 和 supply shares。
  • 池 total supply、total borrow、utilization、interest rate。
  • supply cap 剩余额度。
  • withdrawal rate limit 和当前可提。
  • referral 收益,如果应用支持 referral。

工程旁白

supplyToMarginPool.ts 对应的是借贷池资金端。用户供应后拿到的是供应 position 和 shares,收益来自借款人利息;它不创建 MarginManager 债务,也不会让某个杠杆仓位更安全。

供应页面的风险提示应包含 utilization 和提款限制。高 utilization 可能带来更高 borrow APR 和 supply APR,但也意味着 vault 可提流动性更紧张,rate limit 可能让大额提款需要等待。

Margin 应用判断

  • 供应入口使用 SupplierCap/position 语义,不依赖交易 manager。
  • 可提金额用 shares 实时换算,并额外显示 rate limit 和 vault balance。
  • 供应成功后刷新 lending position,而不是 margin trading position。

动手检查

  • 供应资产后,用户获得的是 supply shares 还是抵押余额?
  • 当前 pool 是否接近 max utilization,提款是否可能受限?
  • APR 展示是否来自 ProtocolConfig.interest_rate 和当前 utilization?

ch09-06 借入 base 或 quote

返回本章

先看用户路径

这里先从用户动作往回看。“借入 base 或 quote”在应用里通常是一组 PTB 和状态刷新,不是一条孤立 move call;每一步都要同时考虑余额、债务和风险率。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

借款入口是 borrow_baseborrow_quote。应用在构造交易前要确认:

  • DeepBook Pool 在 MarginRegistry 中 enabled。
  • 目标 MarginPool 已通过 enable_deepbook_pool_for_loan 授权该 DeepBook Pool。
  • 当前 manager 没有来自其它 MarginPool 的债务。
  • 借款金额大于 min_borrow
  • 借款后风险率仍高于 min_borrow_risk_ratio
  • Pyth price object 新鲜且 confidence 合格。

借入 base 常用于开空:借 SUI 后卖成 USDC。借入 quote 常用于开多:借 USDC 后买入 SUI。

应用展示“可借额度”时不要只用池可用余额。还要取 min:

  • MarginPool vault 可用余额。
  • 最大利用率下剩余可借额度。
  • 用户风险率约束下的最大借款。
  • 交易规模和订单簿深度。

工程旁白

借入 quote 通常用于买入 base,借入 base 通常用于卖出 base。借款会增加 borrow shares,真实 debt amount 会随 margin_state 利息增长;应用应在借款确认页展示当前 APR 和预估日利息。

可借额度不是一个单一链上字段。后端可以先本地估算,再用 dev inspect 验证最终 PTB;如果用户同时借款并下单,还要把成交后资产结构和滑点纳入风险率估算。

Margin 应用判断

  • 借款按钮旁显示 debt asset、borrow APR 和清算阈值距离。
  • 小额借款要检查 min_borrow,避免 shares 换算为 0 或低于池配置。
  • 借款成功后立即刷新 manager debt shares 和 pool utilization。

动手检查

  • 用户借的是 base 还是 quote,对价格风险方向有什么影响?
  • 可借额度是否同时受池端 max utilization 和用户端 risk ratio 限制?
  • 这笔借款会在同一交易归还吗?如果不会,UI 是否展示持续利息?

ch09-07 通过 DeepBook Pool 执行杠杆买入

返回本章

先看用户路径

这一节从 Margin 用户动作读起:围绕“通过 DeepBook Pool 执行杠杆买入”,先把 manager、collateral、loan、trade 和 repay 的顺序排清楚,再看源码入口。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

杠杆买入常见路径:

  1. 用户有 quote 抵押,如 USDC。
  2. borrow_quote 借入更多 USDC。
  3. pool_proxy.place_market_orderplace_limit_order 买入 base。
  4. pool_proxy.withdraw_settled_amounts 结算成交资产。
  5. 重新读取 manager_state 和风险率。

pool_proxy.place_market_order 会先根据订单簿计算 effective price,再调用 registry.assert_price。如果订单簿没有足够流动性,会触发 ENoLiquidityInOrderbook 或 DeepBook Pool 侧错误。

交易前模拟要输出三类信息:

  • 预估成交均价和滑点。
  • 借款后风险率和成交后风险率。
  • 价格保护失败、流动性不足、风险率不足时的用户提示。

工程旁白

杠杆买入的债务通常是 quote。base 价格下跌会降低资产价值并压低风险率;quote 利息增长也会逐步抬高 debt amount。因此交易确认页要同时显示价格风险和借款成本。

市价买入最容易在订单簿深度不足时产生失败或过大滑点。pool_proxy 会做 effective price 与 oracle 检查,应用侧也应在提交前用订单簿快照给出最坏可接受价格。

Margin 应用判断

  • 买入 PTB 包含 borrow quote、place order、settle/refresh 三个逻辑步骤。
  • 所有价格和滑点提示都要标明基于当前订单簿,最终以 dry run 为准。
  • 成交后如果没有立即还款,继续展示 quote debt 的利息累计。

动手检查

  • 买入后 debt asset 是否仍是 quote,base 下跌对风险率有什么影响?
  • 订单失败是 risk ratio 不足、oracle price out of bounds 还是 orderbook liquidity 不足?
  • 交易完成后是否刷新 settled amounts 和 open orders?

ch09-08 通过 DeepBook Pool 执行杠杆卖出

返回本章

先看用户路径

这里先从用户动作往回看。“通过 DeepBook Pool 执行杠杆卖出”在应用里通常是一组 PTB 和状态刷新,不是一条孤立 move call;每一步都要同时考虑余额、债务和风险率。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

杠杆卖出常见路径:

  1. 用户存入 base 或 quote 抵押。
  2. borrow_base 借入 base。
  3. pool_proxy.place_market_order 卖出 base 得到 quote。
  4. 结算成交资产。
  5. 展示 debt asset 为 base 的风险率。

开空的风险方向和开多相反。base 价格上涨会增加用 quote 折算的 base 债务价值,风险率下降。风控面板要明确显示 debt asset,避免用户只看 quote 余额误判安全。

当 Pool 被禁用常规 Margin 交易时,应用应改用 reduce-only 入口帮助用户降风险:

  • place_reduce_only_limit_order
  • place_reduce_only_market_order

这些入口只允许减少净债务方向的交易,不能扩大仓位。

工程旁白

杠杆卖出后,用户通常持有 quote 资产但欠 base。base 价格上涨会让还款成本变高,风险率下降;仅显示 quote 余额会掩盖真实风险。

当 Pool 禁用常规 Margin 交易时,reduce-only 卖出/买回入口是用户自救路径。应用应根据当前 debt asset 和订单方向判断是否可 reduce-only,而不是简单隐藏交易模块。

Margin 应用判断

  • 做空确认页把 debt asset 用醒目字段展示,并显示 base 上涨时的清算距离。
  • reduce-only 入口单独构造,不复用普通下单参数后端静默切换。
  • 平空时通常需要买回 base 后 repay_base,流程要和卖出开仓分开。

动手检查

  • 卖出后用户欠的是哪种资产,价格上涨还是下跌更危险?
  • 当前订单是否真的减少 base debt 风险,而不是扩大空头?
  • quote 余额足够时,是否还需要先买回 base 才能还款?

ch09-09 部分还款、全部还款和平仓

返回本章

先看用户路径

这一节从 Margin 用户动作读起:围绕“部分还款、全部还款和平仓”,先把 manager、collateral、loan、trade 和 repay 的顺序排清楚,再看源码入口。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

还款入口:

  • repay_base:偿还 base 债务。
  • repay_quote:偿还 quote 债务。

两者都接收 Option<u64> amount。传入 none 表示尽量全额还款,传入 some 表示部分还款。内部会根据当前 debt amount 和 shares 计算 repay_shares,从 manager 的 BalanceManager 取出债务资产,再调用 MarginPool.repay

平仓通常是组合操作:

  1. 取消所有 open orders。
  2. 结算 DeepBook Pool。
  3. 如果资产不是债务资产,先通过 DeepBook 反向交易换成债务资产。
  4. repay_baserepay_quote
  5. 债务清零后提取剩余资产。

错误处理重点:

  • ERepayAmountTooLow:还款金额太小。
  • ERepaySharesTooLow:金额换算出的 shares 为 0。
  • EIncorrectMarginPool:传错了债务对应的 MarginPool。
  • 余额不足:manager 里没有足够债务资产,用户需要先成交或存入。

工程旁白

还款金额和债务 shares 之间存在精度边界。全额还款通常用 none 语义让链上尽量还清;部分还款要确保金额能换算成非零 shares,并且 manager 中有足够债务资产。

平仓不是单个 repay 调用。用户可能需要先取消挂单、结算成交资产,再通过 DeepBook 反向交易把资产换成 debt asset,最后还款并提取剩余余额。

Margin 应用判断

  • 还款 UI 默认显示 debt asset,禁止用非债务资产直接提交 repay。
  • 全额还款后重新读取 borrow shares,确认是否真正清零。
  • 平仓流程每一步都保留 dry run,尤其是反向交易和最终提款。

动手检查

  • 传入 none 和 some amount 的链上行为有什么区别?
  • manager 中是否有足够 debt asset,还是需要先反向交易?
  • 平仓后还有 open orders、settled amounts 或 TPSL 残留吗?

ch09-10 TPSL 订单

返回本章

先看用户路径

这里先从用户动作往回看。“TPSL 订单”在应用里通常是一组 PTB 和状态刷新,不是一条孤立 move call;每一步都要同时考虑余额、债务和风险率。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

TPSL 是 manager 内部的条件订单,不是 DeepBook 原生挂单。用户创建 TPSL 时,应用需要构造:

  • conditional_order_id:用户维度唯一 ID。
  • Conditiontrigger_below_pricetrigger_price
  • PendingOrder:限价或市价参数,包括 quantityis_bidpay_with_deep、过期时间等。

执行是 permissionless。后台 worker 可以定时调用 execute_conditional_orders,每次限制 max_orders_to_execute,避免单笔交易 gas 不可控。

UI 要展示四类终态事件:

  • ConditionalOrderExecuted
  • ConditionalOrderCancelled
  • ConditionalOrderInsufficientFunds
  • ConditionalOrderPriceOutOfBounds

创建 TPSL 前必须检查它是否会扩大风险。如果是止损平仓,应优先使用 reduce-only 语义设计交易方向。

工程旁白

TPSL worker 只负责在条件满足时提交执行交易,不保证成交成功。它仍然要面对 oracle 价格保护、资金不足、Pool 暂停、订单簿深度不足等失败面,所以事件订阅和重试策略与创建订单同等重要。

止盈止损在 Margin 场景里应优先服务降风险。借 quote 开多的止损通常是卖出 base 换 quote,借 base 开空的止损通常是买回 base;方向反了会扩大债务风险。

Margin 应用判断

  • 创建表单用 debt asset 推导推荐方向,并允许用户明确确认非 reduce-only 订单。
  • worker 执行批量订单时限制 max_orders_to_execute,并记录每个失败事件。
  • 取消 TPSL 后从 UI 列表移除前等待链上事件或交易确认。

动手检查

  • 这个 TPSL 是止盈、止损还是可能扩大仓位的条件开仓?
  • 触发价格、订单价格和 oracle 价格保护之间是什么关系?
  • worker 失败后用户看到的是等待重试、资金不足还是价格越界?

ch09-11 清算机器人

返回本章

先看用户路径

这一节从 Margin 用户动作读起:围绕“清算机器人”,先把 manager、collateral、loan、trade 和 repay 的顺序排清楚,再看源码入口。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

清算扫描器的职责是找出 risk_ratio < liquidation_risk_ratio 的 manager,并提交有利润的清算交易。

扫描流程:

  1. 获取每个 Pool 的 base/quote MarginPool、risk params 和 Pyth price object。
  2. 枚举注册的 MarginManager 或消费 indexer 事件。
  3. 调读接口或 dev inspect 获取 manager_state、债务 shares 和风险率。
  4. 过滤低于清算阈值的仓位。
  5. 检查 liquidation vault 是否有足够债务资产。
  6. liquidation_vault.liquidate_baseliquidate_quote

fundLiquidationVault.ts 展示了管理员注资:

client.deepbook.marginLiquidations.deposit(
  vaultId,
  liquidationAdminCapID[env],
  "USDC",
  50_000,
)(tx);

机器人需要监控 vault 的 base/quote/DEEP 余额。liquidation_vault.move 还提供 swap_base_to_quoteswap_quote_to_base,可用 DeepBook Pool 调整 vault 库存。

工程旁白

清算机器人不能只看缓存风险率。提交前应重新 dev inspect liquidate_baseliquidate_quote,确认仓位仍低于阈值、vault 有足够 debt asset、预期奖励覆盖 gas 和滑点。

vault 库存是机器人成功率的核心约束。base debt 需要 base 库存,quote debt 需要 quote 库存;swap_base_to_quoteswap_quote_to_base 可用于再平衡,但再平衡本身也依赖 DeepBook Pool 流动性和价格保护。

Margin 应用判断

  • 扫描器按 pool 分片,避免一个热池拖慢所有市场。
  • 清算前记录 price timestamp 和 dev inspect 结果,便于解释失败或竞争丢单。
  • 授权 trader 密钥只持有清算权限,不持有 admin 初始化权限。

动手检查

  • 目标 manager 当前是否仍低于 liquidation threshold?
  • vault 是否持有对应 debt asset,还是需要先 rebalance?
  • 预期 user reward、pool reward、gas 和潜在 default 是否让这笔清算值得执行?

ch09-12 UI 风控展示

返回本章

先看用户路径

这里先从用户动作往回看。“UI 风控展示”在应用里通常是一组 PTB 和状态刷新,不是一条孤立 move call;每一步都要同时考虑余额、债务和风险率。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

仓位页至少展示:

  • manager ID、pool key、owner。
  • base balance、quote balance、DEEP balance。
  • base debt、quote debt、debt asset。
  • risk ratio 和四个阈值。
  • borrow shares 和折算 debt amount。
  • MarginPool utilization、borrow rate、supply rate 估算。
  • Pyth 价格更新时间、confidence 状态。
  • open orders、locked balance、TPSL 数量。
  • 可借额度、可提额度、清算距离。

健康度可以做成区间:

  • 安全:risk ratio 高于 min_withdraw_risk_ratio
  • 注意:低于提款阈值但高于借款阈值。
  • 危险:接近 liquidation_risk_ratio
  • 可清算:低于 liquidation_risk_ratio

每次用户输入借款、提款、下单、TPSL 参数时,都要重新模拟风险率,而不是等点击提交才报错。

工程旁白

风控面板要围绕 debt asset 组织信息。用户最需要知道的是欠什么、欠多少、利息如何增长、哪个价格方向会触发清算,而不是只看账户净值。

输入框每次变化都应触发本地估算和必要的 dry run。比如增加借款、提款或创建 TPSL,都会改变未来风险率;等到提交交易才发现失败,会让用户无法判断该补抵押还是降低规模。

Margin 应用判断

  • 风险率条展示 min withdraw、min borrow、liquidation、target liquidation 四个刻度。
  • 余额表分为 manager collateral、MarginPool supply position、open orders 和 settled amounts。
  • 价格组件显示 Pyth 更新时间和 stale 状态,避免用户误信过期健康度。

动手检查

  • 用户当前风险由债务资产价格、利息增长还是订单锁仓主导?
  • UI 是否把 lending supply shares 与 margin collateral 分开显示?
  • 用户输入一笔新交易后,风险率预览是否即时更新?

ch09-13 错误提示和交易前模拟

返回本章

先看用户路径

这一节从 Margin 用户动作读起:围绕“错误提示和交易前模拟”,先把 manager、collateral、loan、trade 和 repay 的顺序排清楚,再看源码入口。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

应用需要维护 Move abort 到业务提示的映射。常见映射:

  • EBorrowRiskRatioExceeded:借款后风险率不足,请降低借款或增加抵押品。
  • EWithdrawRiskRatioExceeded:提款后风险率不足,请减少提款或先还款。
  • ECannotHaveLoanInMoreThanOneMarginPool:同一 manager 当前只能从一个 MarginPool 借款。
  • EDeepbookPoolNotAllowedForLoan:该交易对暂未被授权使用目标借贷池。
  • EPoolNotEnabledForMarginTrading:该 Pool 的 Margin 交易未启用或已暂停。
  • EInvalidDeposit:只能存入 base、quote 或 DEEP。
  • ECannotLiquidate:仓位尚未达到清算条件。
  • oracle 相关错误:价格过期、置信区间过大或价格源不匹配。

交易前检查顺序:

  1. 本地静态检查:参数范围、余额、pool key、manager owner。
  2. 链上读取:PoolConfig、MarginPool 状态、manager_state、oracle 更新时间。
  3. 本地估算:风险率、滑点、利息、费用。
  4. dry run 或 dev inspect:确认 Move 调用真实可执行。
  5. 提交交易并订阅事件更新 UI。

工程旁白

交易前模拟应该输出状态差异,而不仅是“success/fail”。对借款加交易的 PTB,至少展示借款前后 debt amount、成交后资产、风险率变化、预估利息和可能的价格保护失败点。

错误映射要按用户可行动作设计:风险率不足提示减少金额或补抵押;oracle stale 提示等待更新;pool 未启用提示只能 reduce-only 或稍后再试;min borrow/repay 提示提高金额或选择全额。

Margin 应用判断

  • 本地静态检查不替代 dry run,只用于减少明显错误。
  • 记录 abort module、function、code、PTB 输入和链上对象版本,方便复现。
  • 对于 stale price 和 orderbook liquidity,错误提示要附带刷新或调整参数的操作。

动手检查

  • 这个错误用户能通过改金额解决,还是必须等待维护者/价格更新?
  • dry run 是否使用了和提交交易相同的 shared object 与 price object?
  • 错误映射是否覆盖了 Margin 借贷和 DeepBook Pool 撮合两类 abort?

ch09-14 实战:命令行杠杆交易工具

返回本章

先看用户路径

这里先从用户动作往回看。“实战:命令行杠杆交易工具”在应用里通常是一组 PTB 和状态刷新,不是一条孤立 move call;每一步都要同时考虑余额、债务和风险率。

源码入口

重点阅读:

  • scripts/transactions/marginPrep.ts
  • book/ch09/code/s01-create-margin-manager/README.md
  • book/ch09/code/s03-borrow-and-trade/README.md
  • book/ch09/code/s04-repay-and-close/README.md

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

CLI 可以按命令拆分:

pnpm tsx margin.ts manager create --pool SUI_USDC
pnpm tsx margin.ts collateral deposit --pool SUI_USDC --coin USDC --amount 1000
pnpm tsx margin.ts borrow --pool SUI_USDC --asset USDC --amount 500
pnpm tsx margin.ts trade market --pool SUI_USDC --side buy --quantity 100
pnpm tsx margin.ts repay --pool SUI_USDC --asset USDC --all
pnpm tsx margin.ts state --pool SUI_USDC

每个命令共享同一套配置加载、对象查询、dry run 和错误映射。输出应固定包含 transaction digest、changed objects、事件摘要和最新风险率。

工程旁白

CLI 的价值在于把复杂 PTB 分解成可重复的命令。每条命令都应支持 --dry-run-only,并输出对象 ID、type arguments、gas 估算、事件摘要和风险率变化,便于定位问题。

命令层要明确区分 collateral depositpool supplyborrow。这三个命令都涉及资金,但分别改变 manager collateral、MarginPool supply shares 和 manager debt shares。

Margin 应用判断

  • 配置读取和对象查询做成共享模块,避免每条命令重复硬编码 ID。
  • 输出格式保留 JSON 模式,方便风控面板或测试脚本消费。
  • 危险命令如 borrow、trade、withdraw 默认先预览,用户确认后再提交。

动手检查

  • 这个命令改变 collateral、supply shares 还是 borrow shares?
  • CLI 输出是否足够复现失败交易,包括对象 ID 和 abort 映射?
  • state 命令是否展示利息后的 debt amount,而不只是 borrow shares?

ch09-15 实战:Margin 风控面板

返回本章

先看用户路径

这一节从 Margin 用户动作读起:围绕“实战:Margin 风控面板”,先把 manager、collateral、loan、trade 和 repay 的顺序排清楚,再看源码入口。

源码入口

重点阅读:

源码旁白:先定位结构体、入口函数和事件,再回到本节的资金路径或应用流程。不要从 helper 函数开始读。

把 PTB 串起来

风控面板可以由三张表和一个详情页组成:

  • Pool 表:pool key、enabled、base/quote MarginPool、risk params、当前价格、价格更新时间。
  • Lending 表:asset、total supply、total borrow、utilization、borrow rate、supply cap、vault balance。
  • Manager 表:owner、manager ID、debt asset、risk ratio、debt amount、base/quote asset、TPSL 数量。
  • 详情页:订单、成交、抵押事件、借还事件、清算事件和 dry run 面板。

后台同步优先消费事件:

  • MarginManagerCreatedEvent
  • DepositCollateralEvent
  • WithdrawCollateralEvent
  • LoanBorrowedEvent
  • LoanRepaidEvent
  • LiquidationEvent
  • TPSL 相关事件
  • AssetSuppliedAssetWithdrawn

风险率要定时用最新 Pyth 价格刷新。事件只能告诉你状态发生过变化,不能替代当前价格下的健康度计算。

工程旁白

风控面板不是纯事件列表。事件用于发现状态变化,但风险率、利息和可清算状态必须用最新 price object 与 pool state 重新计算;否则价格剧烈变化时,面板会显示过期健康度。

运营视角需要同时看借贷池和交易账户:某个池 utilization 过高会影响新增借款和提款,某个 manager 风险率过低会影响清算队列,vault 库存不足会影响机器人执行率。

Margin 应用判断

  • 数据层按 pool key 聚合 registry、MarginPool、manager 和 oracle 信息。
  • 面板标注数据来源和刷新时间,尤其是 risk ratio 与 price timestamp。
  • 清算队列表包含 vault 库存检查和最近一次 dev inspect 结果。

动手检查

  • 面板展示的是实时链上读取、定时刷新结果还是 indexer 缓存?
  • 哪个池的 utilization 或 stale price 正在影响最多用户操作?
  • 清算候选是否同时满足风险率、vault 库存和预期收益条件?

ch10 DeepBook Predict 协议源码精读

本章目标

官方文档把 DeepBook Predict 定位为 Sui 上基于到期日的预测市场协议,并明确当前公共集成目标是 Testnet。它支持 binary positions、vertical ranges、oracle-driven prices、PredictManager、Vault liquidity 和 PLP LP shares。本章必须先把这个网络状态说清楚,再进入源码。

读完本章,读者应该能从源码层面解释 DeepBook Predict 的核心对象、交易入口、资金路径、oracle 生命周期、vault 风险和 LP 份额,并能区分:官方 Testnet 集成面、public Predict server 数据面、本书锁定 GitHub 源码快照,以及未来 Mainnet 部署前可能变化的部分。

本章使用的源码快照位于 packages/predict。官方文档当前指出 Predict 来自 predict-testnet-4-16 分支,公共集成目标在 Sui Testnet;因此本书不能把 package IDs、object layouts、entry points 或 public server 行为写成永久主网事实。

官方集成基线

Predict 应用不是只读链,也不是只读 server。官方推荐的集成模型可以整理成三层:

数据来源适合场景本书展开
Public Predict server页面渲染、市场列表、portfolio、vault summary、历史数据ch11、ch13 会把它当作读模型和产品数据源。
Sui checkpoint / event streaming需要低延迟 oracle 更新的页面或策略ch10-05、ch10-12 讲 oracle event 和 freshness。
Direct onchain object reads钱包交易前后需要确认关键状态ch10 源码章和 ch12 SDK 章都会保留直接对象读取路径。

版本旁白:Predict 是本书里最需要标注版本状态的章节。写法上要避免“源码里有函数,所以产品已经稳定”的推断;官方 Testnet 文档、public server、源码分支和本地锁定提交必须分别说明。

本章学习阶梯

  • L2 先理解 Predict 的产品模型:区间、到期、oracle、LP vault。
  • L3 读 predict.move、manager、vault、pricing、risk 和 fee reserve。
  • L4 能解释 mint、redeem、settle、withdraw 的状态变化。
  • L5 能判断 Predict 当前网络/版本边界和可产品化范围。

关键定义卡片

Predict 的核心对象不是订单簿,而是预测市场共享状态:

public struct Predict has key {
    id: UID,
    vault: Vault,
    fee_reserve: FeeReserve,
    treasury_cap: TreasuryCap<PLP>,
    pricing_config: PricingConfig,
    risk_config: RiskConfig,
    treasury_config: TreasuryConfig,
    oracle_config: OracleConfig,
    withdrawal_limiter: RateLimiter,
    trading_paused: bool,
}

这段定义说明 Predict 的主线是 vault 风险、oracle、pricing 和 LP 份额,不是 price level 撮合。trading_paused 是全局交易开关,withdrawal_limiter 约束 LP 退出,fee_reserve 把 LP、协议和保险费用从 vault 价值中拆出来。

核心交易入口:

public fun mint<Quote>(
    predict: &mut Predict,
    manager: &mut PredictManager,
    oracle: &OracleSVI,
    key: RangeKey,
    quantity: u64,
    clock: &Clock,
    ctx: &mut TxContext,
)

public fun redeem<Quote>(
    predict: &mut Predict,
    manager: &mut PredictManager,
    oracle: &OracleSVI,
    key: RangeKey,
    quantity: u64,
    clock: &Clock,
    ctx: &mut TxContext,
)

mintredeem 都需要 PredictManager、oracle、range key、clock 和 quote 类型。应用开发时如果只展示“买入/卖出”按钮,而不展示 oracle 状态、区间、到期和费用,就解释不了交易为什么失败。

源码地图

小节目录

本章代码

  • book/ch10/code/s01-predict-source-map/:生成 Predict 模块依赖图。
  • book/ch10/code/s02-market-key-builder/:构造 RangeKey 所需参数。
  • book/ch10/code/s03-pricing-calculator/:实现简化 fee 计算器。
  • book/ch10/code/s04-vault-accounting/:模拟 Predict vault、PLP、fee reserve 和 payout 的账本变化。

Move 高阶穿插点

  • Predict 是学习 Move 定价型协议的案例:头寸不是普通订单,而是区间、到期、oracle 和 vault 风险的组合。
  • 读配置模块时区分全局参数、单 oracle override、资产级边界和管理员权限。
  • 事件不仅服务历史展示,也服务用户理解费用、赎回、结算和 LP 份额变化。

常见错误

  • 把 Testnet integration surface 写成 Mainnet 稳定能力。
  • 把旧 Predict 实验 package ID 或本地脚本配置写进正文,覆盖官方当前 contract information。
  • 把 quote fee 当成 bps;源码中它是每单位绝对价格增量。
  • 只用 strike 和方向当市场 ID,忽略 oracle ID 与 expiry。
  • LP 页面只展示 vault balance,不展示 MTM、max payout 和 withdrawal limiter。
  • 把 settled redeem 写成收费交易;源码中 settled redeem 是 zero-fee。
  • 把 Predict vault 误写成 DeepBookV3 spot pool 或 MarginPool。

本章检查清单

  • 能解释 PredictRegistryPredictManagerOracleSVIVaultPLP 的对象关系。
  • 能写出 mint 的资金路径:manager withdraw、fee split、vault accept payment、position increase。
  • 能解释 live redeem 与 settled redeem 的区别。
  • 能说明 RangeKey 为什么必须包含 oracle 和 expiry。
  • 能说明 pricing config 与 risk config 分别控制什么。
  • 能区分本书采用的 GitHub 源码快照能力和迁移计划未完成事项。

进阶练习

  1. 设计一个 SUI 价格 UP/DOWN 市场,写出 oracle asset、feed id、expiry、strike grid、两个 RangeKey
  2. 给定 vault balance、total MTM、max payout 和 fee 参数,计算 LP 当前 NAV 与用户 mint 的 all-in cost。
  3. 为 Predict 设计最小事件索引表,至少覆盖 market、oracle update、position、LP share 和 fee accrual。

ch10-01 版本状态、网络状态和产品边界

返回本章

先看市场问题

读“版本状态、网络状态和产品边界”时先抓参数边界。Predict 的关键不是某个函数名,而是 oracle、expiry、strike、vault 和 payout 之间的关系。

源码入口

  • PREDICT_MIGRATION.md:PR 1/2 合入状态与后续阶段的计划状态。
  • packages/predict/sources/oracle.movevault/vault.moveconfig/pricing_config.moveconfig/risk_config.move:迁移文档中已合入能力的核心文件。
  • packages/predict/sources/predict.moveregistry.movepredict_manager.movemarket_key/range_key.move:本书采用的 GitHub 源码快照中可读到的后续模块,生产状态需要另行核对。

读市场参数

Predict 是 DeepBook 生态里的预测市场/二元期权协议,不是 DeepBookV3 spot order book,也不是 DeepBook Margin 借贷池。它复用 DeepBook 的 BalanceManager 账户模型,但资金池、oracle、PLP 和结算逻辑都在 packages/predict 内部实现。

从迁移计划看,稳定事实是 PR 1 与 PR 2:oracle.movemath.moveconstants.movevault.movepricing_config.moverisk_config.move 已列为 merged。PR 3 到 PR 11 仍标记为 next、blocked 或未开始。GitHub 源码已经出现更多模块,适合源码精读和本地仿真,但生产应用必须按实际部署包、审计状态和网络公告再确认,不能假设 Predict Indexer、Predict Server、部署脚本和 Oracle 服务已经主网稳定。

文档写作时建议把 Predict 标为“本书采用的 GitHub 源码快照 + 迁移中协议”。如果要在应用里展示网络状态,应把 package ID、registry ID、predict object ID、oracle ID 和发布网络都放进配置,并附带 migrationStatusreleaseChannel 字段,避免 UI 把示例对象误读为主网。

本章后续小节提到的 Indexer、Server、Oracle service 都只作为需求或未来读模型讨论。可靠的链上事实来自 Move 对象、事件和 dry run 结果;链下服务在迁移文档未完成前不能写成稳定依赖。

Predict 边界判断

  • 每个 Predict 说明都先标注来源是 PREDICT_MIGRATION.md 还是本书采用的 GitHub 源码快照。
  • 写主网能力时必须有实际 package/object ID 和部署状态;没有时只写本地仿真或接口设计。
  • 把 Spot、Margin、Predict 的资金池分开描述,不把 Predict vault 写成 DeepBook pool。

动手检查

  • 哪些模块在迁移文档中明确 merged,哪些只是GitHub 源码可见?
  • 如果前端配置里没有 Predict package ID 和版本状态,应该阻止哪些交易入口?
  • 为什么不能把 Predict Indexer/Server 写成已经上线的稳定能力?

ch10-02 预测市场、二元期权、区间市场和 LP vault

返回本章

先看市场问题

这里先从市场定义往下看。“预测市场、二元期权、区间市场和 LP vault”服务的是 Predict 市场如何被定义、定价、mint、settle 或索引;不要把它读成普通现货订单簿的变体。

源码入口

  • packages/predict/sources/market_key/range_key.move(oracle, expiry, lower, higher] 的 market key 定义。
  • packages/predict/sources/predict.movemintredeemsupplywithdraw 的外部入口。
  • packages/predict/sources/vault/vault.movevault/plp.move:LP 资金池、敞口和 PLP 份额。

读市场参数

Predict 的交易单位是一个在到期时支付 0 或 quantity 的区间头寸。range_key.move 把头寸定义为 (oracle_id, expiry, lower_strike, higher_strike),结算规则是价格落在 (lower, higher] 内则支付 quantity,否则支付 0。二元 UP 市场可以表示为 (strike, +inf],DOWN 市场可以表示为 (-inf, strike],更宽的区间市场则直接设置两个有限 strike。

LP vault 是交易对手方。用户 mint 时从 PredictManager 扣 quote,vault 接收 fair value 和 LP fee,同时记录潜在赔付;用户 live redeem 时 vault 支付当前 fair value 扣 fee;到期后 redeem 按 settlement price 精确支付。LP 通过 predict.move::supply 获得 PLP,通过 withdraw 烧毁 PLP 取回 vault value 的份额。

PTB 构造上,二元市场和区间市场的差异只体现在传给 mint<Quote>RangeKey。UP 可以使用正无穷 sentinel 作为 upper bound,DOWN 可以使用负无穷 sentinel 作为 lower bound;有限区间则传两个 strike。无论 UI 标签怎么写,链上 key 都必须携带 oracle ID 和 expiry。

LP 不是被动收取固定利息,而是在 vault 中承担所有未结算区间的对手方风险。服务层报价时应同时返回 fair value、fee、all-in ask、vault MTM、max payout 和是否满足 exposure limit。

Predict 边界判断

  • 市场卡片必须展示 oracle、expiry、strike range 和 quote asset,而不是只显示 UP/DOWN。
  • mint 前检查 PredictManager 余额、oracle 新鲜度、ask bounds 和 vault exposure。
  • LP 页面同时展示 PLP NAV、MTM、max payout、withdraw limiter 和 settlement 状态。

动手检查

  • 为什么同一个 strike 在不同 oracle 或 expiry 下不是同一个市场?
  • 用户 live redeem 和 settled redeem 分别由 vault 支付什么金额?
  • LP vault 的 balancetotal_mtmtotal_max_payout 对 UI 风险提示有什么影响?

ch10-03 predict.move 的对外入口

返回本章

先看市场问题

读“predict.move 的对外入口”时先抓参数边界。Predict 的关键不是某个函数名,而是 oracle、expiry、strike、vault 和 payout 之间的关系。

源码入口

  • packages/predict/sources/predict.movePredict 对象、mintredeemredeem_permissionlesssupplywithdraw
  • packages/predict/sources/predict_manager.move:manager owner 校验、余额 withdraw、头寸增减。
  • packages/predict/sources/accounting/fee_reserve.movevault/vault.movevault/plp.move:费用、vault 资产和 PLP 铸烧。

关键定义

Predict 的主对象定义:

public struct Predict has key {
    id: UID,
    vault: Vault,
    fee_reserve: FeeReserve,
    treasury_cap: TreasuryCap<PLP>,
    pricing_config: PricingConfig,
    risk_config: RiskConfig,
    treasury_config: TreasuryConfig,
    oracle_config: OracleConfig,
    withdrawal_limiter: RateLimiter,
    trading_paused: bool,
}

两个最重要的交易入口:

public fun mint<Quote>(
    predict: &mut Predict,
    manager: &mut PredictManager,
    oracle: &OracleSVI,
    key: RangeKey,
    quantity: u64,
    clock: &Clock,
    ctx: &mut TxContext,
)

public fun redeem<Quote>(
    predict: &mut Predict,
    manager: &mut PredictManager,
    oracle: &OracleSVI,
    key: RangeKey,
    quantity: u64,
    clock: &Clock,
    ctx: &mut TxContext,
)

这两个签名解释了 Predict 应用为什么比 Spot 应用更依赖上下文:交易不仅需要 manager 和共享对象,还需要 oracle、range key、clock 和 quote 类型。错误也不只来自余额不足,还可能来自交易暂停、oracle stale、区间非法、ask price 越界或 vault 风险限制。

读市场参数

predict.move 的核心共享对象是 Predict,字段包括 vaultfee_reservetreasury_cap<PLP>pricing_configrisk_configtreasury_configoracle_configwithdrawal_limitertrading_paused

交易入口 mint<Quote>(predict, manager, oracle, key, quantity, clock, ctx) 要求 ctx.sender() == manager.owner(),调用 mint_internal。内部先校验 quote asset、trading pause、oracle 和 range key,再通过 quote_mint_amounts 得到 principal 与 fee,从 PredictManager withdraw quote,fee 进入 FeeReserve,LP share 回流 vault,剩余 payment 进入 vault,最后 manager.increase_position 并触发 PositionMinted

redeem<Quote> 根据 oracle.is_settled() 分支。未结算时走 redeem_live_internal,用户卖回当前 fair value 并支付 live redeem fee;已结算时走 redeem_settled_internal,按 RangeKey::settled_payout 支付,结算 redeem 不收费。redeem_permissionless 允许任何执行者把已结算头寸卖入 manager 余额,适合 keeper 或自动领取。

LP 入口是 supply<Quote>withdraw<Quote>supply 接收 quote coin,刷新 LP 相关 MTM,按 amount * total_plp / vault_value 计算 shares;首次供应时按 amount mint PLP。withdraw 先校验 MTM freshness,再按 shares 占比计算 amount,消耗 withdrawal limiter,烧毁 PLP 并从 vault 支付 quote。

应用层封装 mint 的 PTB 顺序应先保证 manager 已存在并有 quote 余额,然后传入 predictmanageroracleRangeKeyquantityClock。错误解析应优先判断 sender 是否等于 manager owner、quote asset 是否在 treasury whitelist、oracle 是否 stale 或已 settle、ask price 是否越界。

redeem_permissionless 是自动化领取的基础,但它不代表存在稳定 keeper 服务。文档和 SDK 可以设计 keeper 调用接口,却必须标注当前迁移状态,不能把它描述成已经上线的 Predict Server 能力。

Predict 边界判断

  • 把每个入口的输入对象、type argument、sender 约束和事件分开列清楚。
  • mint 说明必须覆盖 manager withdraw、fee split、vault accept payment、position increase。
  • withdraw 说明必须包含 MTM freshness、rate limiter、PLP burn 和 quote payout。

动手检查

  • mint_internal 失败最常见会落在 owner、quote asset、oracle、range key 还是 ask bounds?
  • 为什么 settled redeem 是 zero-fee,而 live redeem 需要 fee?
  • redeem_permissionless 适合什么自动化场景,为什么不能据此假设稳定 Server 已存在?

ch10-04 predict_manager.move 的用户账户和头寸管理

返回本章

先看市场问题

这里先从市场定义往下看。“predict_manager.move 的用户账户和头寸管理”服务的是 Predict 市场如何被定义、定价、mint、settle 或索引;不要把它读成普通现货订单簿的变体。

源码入口

  • packages/predict/sources/predict_manager.movePredictManager 字段、deposit、withdraw、position 增减。
  • packages/predict/sources/registry.move:创建和分享 manager 的入口与派生 key。
  • packages/deepbook/sources/balance_manager.move:Predict 内部复用的余额模型。

关键定义

PredictManager 把用户余额和预测头寸放在一个共享对象里:

public struct PredictManagerKey(address, u64) has copy, drop, store;

public struct PredictManager has key {
    id: UID,
    owner: address,
    balance_manager: BalanceManager,
    deposit_cap: DepositCap,
    withdraw_cap: WithdrawCap,
    positions: Table<RangeKey, u64>,
}

公开给用户的资金入口:

public fun deposit<T>(self: &mut PredictManager, coin: Coin<T>, ctx: &TxContext)

public fun withdraw<T>(
    self: &mut PredictManager,
    amount: u64,
    ctx: &mut TxContext,
): Coin<T>

包内头寸更新入口:

public(package) fun increase_position(self: &mut PredictManager, key: RangeKey, quantity: u64)
public(package) fun decrease_position(self: &mut PredictManager, key: RangeKey, quantity: u64)

positions 只保存区间数量,不保存成本价、手续费或 PnL。应用要展示收益,必须结合 mint/redeem 事件、交易 digest 或未来 Indexer 读模型。

读市场参数

PredictManager 是每个用户的共享对象,字段包括 owner、DeepBook BalanceManagerDepositCapWithdrawCapTable<RangeKey, u64> positions。registry.move::create_and_share_manager 使用 PredictManagerKey(address, 0) 派生对象 ID,v1 默认每个地址一个 manager。

用户资金先通过 deposit<T>(manager, coin, ctx) 进入内部 BalanceManager,交易时 predict.move 用包内权限调用 manager.withdraw<Quote>(cost, ctx)。头寸只记录 range quantity,不记录成本价;应用层要从事件、交易历史或未来 Indexer 里恢复成本和 PnL。

服务层通常提供 ensurePredictManager(owner):先按 PredictManagerKey(address, 0) 查询对象,不存在再构造创建 PTB。创建之后,后续 deposit、mint、redeem 都使用同一个 manager 对象,避免用户在多个 manager 间分散余额和头寸。

因为 positions table 只保存 quantity,成本价、fee、mint 时间和 realized PnL 需要从交易事件或未来 Indexer 读模型恢复。当前迁移状态下,应用可以本地记录提交摘要和 digest,但不能把未完成的 Predict Indexer 当成唯一账本。

Predict 边界判断

  • manager 创建、deposit、mint 应拆成可组合 PTB,允许前端一次签名完成初始化。
  • 任何交易都校验 ctx.sender() 与 manager owner 一致,避免代签或错误对象。
  • PnL 展示明确区分链上 position quantity 和链下成本记录。

动手检查

  • 为什么 manager ID 派生要包含 owner,而 market key 不包含 owner?
  • 如果用户换钱包或误选别人的 manager,Move 层会在哪里拒绝?
  • 没有稳定 Indexer 时,应用如何保存和恢复用户成本信息?

ch10-05 oracle.moveoracle_config.move 的价格输入

返回本章

先看市场问题

读“oracle.move 与 oracle_config.move 的价格输入”时先抓参数边界。Predict 的关键不是某个函数名,而是 oracle、expiry、strike、vault 和 payout 之间的关系。

源码入口

  • packages/predict/sources/oracle.moveOracleSVI、spot 更新、basis、SVI、settlement。
  • packages/predict/sources/oracle_config.move:asset feed id、basis bounds、ask bounds、strike grid 配置。
  • packages/predict/simulations/src/sim.ts:本地仿真中 oracle refresh 与 mint 合并的交易路径。

关键定义

OracleConfig 是创建 oracle 前的配置表。它保存 feed id、basis bounds、ask bounds、strike grid 和 staleness 阈值;创建具体 oracle 时,这些值会被快照到 oracle 相关对象中。

public struct OracleConfig has store {
    oracle_grids: Table<ID, OracleGrid>,
    oracle_ask_bounds: Table<ID, AskBounds>,
    asset_basis_bounds: Table<String, BasisBounds>,
    asset_feed_ids: Table<String, u64>,
    spot_staleness_threshold_ms: u64,
    basis_staleness_threshold_ms: u64,
    lazer_authoritative_threshold_ms: u64,
    lazer_settlement_authoritative_threshold_ms: u64,
}

public struct CurvePoint has copy, drop, store {
    strike: u64,
    up_price: u64,
}

public fun new_curve_point(strike: u64, up_price: u64): CurvePoint {
    CurvePoint { strike, up_price }
}

CurvePoint 是 Predict 定价里非常适合拿来训练 Move 直觉的结构:它很小,但类型边界很硬。strike 和 up price 都是链上整数精度,不是小数;任何前端图表插值都只能用于展示,不能反过来生成未校验的 mint 参数。

读市场参数

oracle.moveOracleSVI 维护 underlying asset、Pyth Lazer feed id、expiry、spot、basis、SVI 参数、staleness 时间戳和 settlement price。update_spot_from_lazer 面向高频 spot,update_prices 面向 operator 推送 spot/forward 并更新 basis,update_svi 更新波动率曲面。到期后 oracle 进入 settlement 路径,冻结 settlement price。

oracle_config.move 是 Predict 之上的配置层。创建 oracle 前,管理员必须通过 set_asset_feed_id 绑定 asset -> pyth_lazer_feed_id;可以通过 set_asset_basis_bounds 设置 per-asset circuit breaker;创建 oracle 时把 staleness threshold、basis bounds、feed id 和 strike grid 快照到 oracle 或 Predict 配置中。后续修改全局配置不会 retroactively 改老 oracle。

PTB 中不要把 oracle 更新服务当成稳定外部依赖。当前可以在本地仿真里把 refresh 和 mint 放进同一轮 scenario,但生产应用需要先检查迁移状态、operator 权限和 feed 配置,再决定由谁推送价格。

错误解析上,oracle 相关失败通常不是余额问题,而是 feed id 未配置、spot/SVI 超过 staleness threshold、basis 超出 bounds、settlement 已完成或 range key 的 oracle/expiry 与传入 oracle 不匹配。

Predict 边界判断

  • 创建 oracle 前校验 asset、Pyth Lazer feed id、basis bounds 和 strike grid。
  • 交易报价必须展示 oracle 更新时间和是否 stale。
  • 结算逻辑只引用链上 frozen settlement price,不引用前端缓存价格。

动手检查

  • oracle_config 修改后为什么不能假设老 oracle 自动改变?
  • mint 前 oracle stale 和 basis bound 失败应如何提示用户?
  • settlement price 冻结后,live redeem 路径为什么不再适用?

ch10-06 vault/vault.move 的资金池、mint 和 settle

返回本章

先看市场问题

这里先从市场定义往下看。“vault/vault.move 的资金池、mint 和 settle”服务的是 Predict 市场如何被定义、定价、mint、settle 或索引;不要把它读成普通现货订单簿的变体。

源码入口

  • packages/predict/sources/vault/vault.moveVaultinsert_live_rangeremove_live_rangeremove_settled_rangevault_value
  • packages/predict/sources/helper/strike_matrix.move:区间库存、MTM、worst-case payout。
  • packages/predict/sources/config/risk_config.movemax_total_exposure_pctmtm_freshness_ms

读市场参数

VaultBag 保存不同 quote asset 的实际 Balance<T>,并用 balance: u64 维护统一 quote 单位余额。每个 oracle 对应一个 StrikeMatrix,用于记录所有区间头寸的边界变化。insert_live_range 在 mint 后插入区间并刷新 live MTM;remove_live_range 在 live redeem 时移除区间;remove_settled_range 在 settled redeem 时按 settlement 减少 liability。

风险核心是三个数:total_mtm 表示当前 mark-to-market liability,total_max_payout 表示最坏结算赔付,vault_value() 返回 balance - total_mtmassert_total_exposure(max_total_pct)total_mtm <= balance * max_total_pct 限制交易后敞口。compact_settled_oracle_if_needed 会把已结算 oracle 的密集矩阵压缩成固定 liability 状态,减少长期存储压力。

vault 的核心不是单个余额,而是“余额减去未结算负债”的动态净值。mint 增加 quote payment,也增加区间 liability;live redeem 反向移除 live range 并支付 fair value;settled redeem 按 settlement price 移除固定赔付。

服务封装里应把 vault_value 作为 LP NAV 的基础,把 total_max_payout 作为压力场景展示。dry run 如果因为 exposure limit 或 MTM freshness 失败,应提示用户这是 vault 风险约束,不是钱包余额不足。

Predict 边界判断

  • 所有 LP 估值都使用 vault_value() 思路,而不是裸 balance
  • mint 后同时记录 payment、fee、MTM 增量和 max payout 增量。
  • settled oracle compact 是存储优化,不改变用户应得 payout。

动手检查

  • total_mtmtotal_max_payout 分别回答什么风险问题?
  • 为什么 LP withdraw 需要 MTM 新鲜度检查?
  • settled redeem 对 vault 的 balance 和 liability 各有什么影响?

ch10-07 vault/plp.move 的 LP 份额和收益

返回本章

先看市场问题

读“vault/plp.move 的 LP 份额和收益”时先抓参数边界。Predict 的关键不是某个函数名,而是 oracle、expiry、strike、vault 和 payout 之间的关系。

源码入口

  • packages/predict/sources/vault/plp.movePLP token 注册、6 位精度和 treasury cap。
  • packages/predict/sources/predict.movesupply<Quote>withdraw<Quote>
  • packages/predict/sources/helper/rate_limiter.movevault/vault.move:提款限速和 vault value。

读市场参数

plp.move 注册 PLP token,精度为 6,symbol 为 PLP。PLP 不是 claim 某个具体 coin 的简单凭证,而是 Predict vault value 的份额。mint 交易中的 LP fee 会留在 vault,因此会提升 LP NAV;protocol 和 insurance fee 会进入 FeeReserve,不计入 LP vault value。

LP 退出时的风险点有三个:未结算敞口必须有新鲜 MTM;提款可能受 RateLimiter 限制;极端情况下 settlement payout 会把 vault_value 压低,所以前端不能只展示余额,必须展示 total_mtmtotal_max_payout、available withdrawal 和未结算 oracle 列表。

首次 supply 可以按 amount 铸造 PLP,后续 supply 要按当前 vault value 与 total PLP 供应量计算份额。这样 LP fee 留在 vault 时,老 LP 的 NAV 会提高,新 LP 不能用原始 1:1 价格稀释已有收益。

提款 PTB 应先 dry run,读取失败是否来自 rate limiter、MTM freshness 或 vault value 不足。前端不要只给“提款失败”,而要显示当前 available withdrawal、下一次 refill 时间和未结算市场列表。

Predict 边界判断

  • PLP 展示使用 6 位精度,并说明它代表 vault 净值份额。
  • LP fee、protocol fee、insurance fee 分别展示,避免把全部 fee 记入 LP 收益。
  • 提款前检查 limiter 状态和所有 live oracle 的 MTM 更新时间。

动手检查

  • 为什么 LP fee 留在 vault 会提高 PLP NAV?
  • rate limiter disabled 和 enabled 时 withdraw 行为有什么差异?
  • 哪些风险会让 PLP 持有人不能按当前余额立即退出?

ch10-08 range_key.move 的 market key

返回本章

先看市场问题

Predict 市场最怕“看起来一样”的 key。BTC-UP-100000 这样的字符串在 UI 里很直观,但链上并不知道它属于哪个 oracle、哪个到期日、哪个部署环境,也不知道边界价格怎么处理。

RangeKey 解决的是这个问题:把市场身份压成 oracle、expiry、lower strike、higher strike 四个字段。后面的 mint、vault 记账、settlement 和 indexer 都应该围绕这四个字段展开。

源码入口

  • packages/predict/sources/market_key/range_key.moveRangeKey::new、sentinel 校验和 settled payout。
  • packages/predict/sources/helper/constants.move:正负无穷 sentinel 和价格精度常量。
  • book/ch10/code/s02-market-key-builder/README.md:应用侧构造 key 的练习骨架。

关键定义

RangeKey 用四个字段唯一表达一个区间市场。它不是 UI 字符串,也不是数据库自增 id,而是 PredictManager 和 Vault 都能验证的链上 key。

public struct RangeKey has copy, drop, store {
    oracle_id: ID,
    expiry: u64,
    lower_strike: u64,
    higher_strike: u64,
}

public fun new(
    oracle_id: ID,
    expiry: u64,
    lower_strike: u64,
    higher_strike: u64,
): RangeKey {
    assert!(lower_strike < higher_strike, EInvalidStrikes);
    assert!(
        !(lower_strike == constants::neg_inf!() && higher_strike == constants::pos_inf!()),
        EInvalidStrikes,
    );
    RangeKey { oracle_id, expiry, lower_strike, higher_strike }
}

public(package) fun settled_payout(key: &RangeKey, settlement: u64, quantity: u64): u64 {
    settled_range_payout(settlement, key.lower_strike, key.higher_strike, quantity)
}

settled_payout 把区间定义写死为 (lower, higher]。这对产品设计很重要:边界价格恰好等于 lower 时不支付,恰好等于 higher 时支付。前端、合约测试和 indexer 都必须使用同一套边界语义。

读市场参数

RangeKey::new 的两条校验都和产品语义直接相关。第一条要求 lower < higher,避免无效区间;第二条禁止 (-inf, +inf],避免构造必胜市场。lower == constants::neg_inf!() 表示 DOWN sentinel,higher == constants::pos_inf!() 表示 UP sentinel。

应用开发中 market key 应该由 oracle ID、expiry 和 strike grid 一起派生,不能只用字符串 "BTC-UP-100000"。同一个 strike 在不同 oracle、不同 expiry、不同 package 部署中是不同市场。

PTB 构造时不要让前端传任意字符串 market id。服务层应从配置中的 oracle object、expiry、strike grid 和用户选择的方向派生 RangeKey,再把这些字段作为 move call 参数或序列化结构传入。

未来索引表可以把 market_id 设计成 package/network/oracle/expiry/lower/higher 的规范化字符串,但链上判断仍以 RangeKey 字段为准。由于 Predict Indexer 未完成,这里只是读模型建议,不是稳定 API。

Predict 边界判断

  • 市场 URL、缓存 key 和事件解析至少包含 network/package、oracle ID、expiry、lower、higher。
  • 服务层不要让前端传任意 market id 字符串;应由配置和用户选择派生 RangeKey
  • strike 展示可以格式化,提交参数必须保留链上整数精度。

动手检查

  • 为什么 BTC-UP-100000 不是足够安全的 market id?
  • lower == neg_infhigher == pos_inf 分别表示哪类二元市场?
  • 未来 Indexer 如果落库 market key,至少需要哪些字段?

ch10-09 pricing config、risk config、treasury config

返回本章

先看市场问题

读“pricing config、risk config、treasury config”时先抓参数边界。Predict 的关键不是某个函数名,而是 oracle、expiry、strike、vault 和 payout 之间的关系。

源码入口

  • packages/predict/sources/config/pricing_config.move:base fee、min fee、utilization fee、ask bounds。
  • packages/predict/sources/config/risk_config.move:总敞口比例与 MTM freshness。
  • packages/predict/sources/config/treasury_config.move:quote asset whitelist 和 6 位 decimals 校验。

读市场参数

pricing_config.move 的 fee 不是 bps,而是 1e9 scale 的每单位绝对价格增量。quote_fee_rate_from_fair_price 使用 base_fee * sqrt(price * (1 - price)),再叠加 utilization fee,并受 min_fee 约束。predict.move 还会检查 all-in ask price 落在全局或 per-oracle ask bounds 内。

risk_config.move 只有两个字段:max_total_exposure_pctmtm_freshness_ms。前者限制 vault 交易后总 MTM,后者用于 LP supply/withdraw 前检查所有未结算 oracle 的 MTM 是否足够新鲜。treasury_config.move 维护 quote asset 白名单,并要求 quote coin decimals 等于 required_quote_decimals,当前设计与 6 位 quote 单位对齐。

服务报价应同时返回 fair price、fee rate、all-in ask 和触发的配置来源。ask bounds 可以是全局或 oracle/asset 维度,UI 只显示最终是否可交易还不够,应该告诉用户是价格过高、利用率过高还是 quote asset 不被允许。

管理员更新配置时要通过 registry 或 Predict 管理入口,并在交易 bytes 里固定 gas、expiration 和 cap 对象。生产文档只能说“按当前发布版本入口执行”,不能假设迁移中的部署脚本已经稳定。

Predict 边界判断

  • fee 计算文档统一写 1e9 price scale,不写 bps。
  • mint 前同时检查 treasury whitelist、quote decimals、ask bounds 和 exposure。
  • 配置更新记录 digest、版本和生效范围,便于回放报价差异。

动手检查

  • base fee、min fee、utilization fee 分别解决什么问题?
  • quote asset decimals 不等于 6 时为什么应直接拒绝?
  • MTM freshness 和 max exposure 分别会拦截哪类操作?

ch10-10 strike matrix、rate limiter 和 math helper

返回本章

先看市场问题

这里先从市场定义往下看。“strike matrix、rate limiter 和 math helper”服务的是 Predict 市场如何被定义、定价、mint、settle 或索引;不要把它读成普通现货订单簿的变体。

源码入口

  • packages/predict/sources/helper/strike_matrix.move:分页 strike grid、区间库存、MTM、worst-case payout。
  • packages/predict/sources/helper/rate_limiter.move:token bucket capacity、refill、consume。
  • packages/predict/sources/helper/math.movei64.movetuning_constants.move:SVI/Black-Scholes 近似与定点数工具。

读市场参数

strike_matrix.move 用每 512 个 strike 一页的 dense grid 记录区间边界,既支持 live curve MTM,也支持结算时 exact worst-case payout。它不是订单簿,而是 vault 对所有已 mint 区间的库存账本。

rate_limiter.move 是 LP withdraw 的 token bucket。默认 disabled,管理员需要设置 capacity 和 refill rate 后启用。consume 会拒绝超过 capacity 或当前 available 的提款;record_deposit 会在启用时把新增供应加入提款预算。math.movei64.movetuning_constants.move 提供 SVI、Black-Scholes 近似和默认参数,所有价格通常使用 FLOAT_SCALING = 1e9

strike matrix 是 vault 的风险账本,不是交易撮合簿。用户 mint 并不会挂单等待成交,而是直接与 vault 定价成交;matrix 记录成交后 vault 在各个 strike 区间上的赔付曲线。

dry run 报错若来自 rate limiter,服务层可以根据 limiter 状态给出“当前可提数量”和“等待 refill”的提示;若来自 math 或 bounds,通常说明输入价格、strike 或 oracle 曲线不可用。

Predict 边界判断

  • 风险图表用 matrix liability 曲线,不把它画成 order book depth。
  • 提款 UI 显示 limiter capacity、available、refill rate 和是否 enabled。
  • 所有价格计算统一使用源码精度常量,避免 JS 浮点直接参与链上金额。

动手检查

  • strike matrix 为什么适合计算 worst-case payout?
  • rate limiter 如何降低 LP 集体退出对 vault 的冲击?
  • 定点数精度错误会如何影响 ask bounds 或 fee 判断?

ch10-11 fee reserve 和协议收费路径

返回本章

先看市场问题

读“fee reserve 和协议收费路径”时先抓参数边界。Predict 的关键不是某个函数名,而是 oracle、expiry、strike、vault 和 payout 之间的关系。

源码入口

  • packages/predict/sources/accounting/fee_reserve.move:fee reserve 结构和拆分。
  • packages/predict/sources/predict.move:mint/redeem 时调用 fee reserve 与 vault 的路径。
  • packages/predict/sources/config/pricing_config.move:fee rate 来源。

读市场参数

fee_reserve.move 把 charged trade fee 拆成 LP、protocol 和 insurance 三部分。默认比例来自 constants,测试显示 100 fee units 会按默认 60/20/20 拆分,101 fee units 的 rounding dust 留给 LP。predict.move::apply_fee 把 protocol 和 insurance 份额存入 reserve,把 LP 份额重新存入 vault,因此 LP fee 会留在 NAV 中。

settlement redeem 不收费,apply_fee 遇到 0 fee 会直接销毁 zero balance,不发 fee accrual。事件层面,PositionMintedPositionRedeemed 都带 fee_amountFeeAccrued 带 quote asset、total fee 和三路拆分。

mint 的 all-in cost 可以拆成 principal、LP fee、protocol fee、insurance fee。principal 和 LP fee 进入 vault 后影响 LP NAV;protocol/insurance fee 进入 FeeReserve,后续提取需要单独权限和事件记录。

财务报表不要只记录总 fee。至少要按 market key、oracle、expiry、quote asset、fee bucket 和交易 digest 归档。由于 Predict Indexer 仍是未来需求,当前可以在应用侧保存提交摘要,但不能声称已有稳定索引表。

Predict 边界判断

  • 报价响应展示 principal、LP fee、protocol fee、insurance fee 和 all-in ask。
  • 报表口径明确 vault 收益与 protocol/insurance reserve 的边界。
  • 错误解析把 fee 过高、ask bound 失败和余额不足拆开提示。

动手检查

  • 哪些 fee 会提高 PLP NAV,哪些不会?
  • 为什么 settled redeem 不应再收取交易费?
  • 如果未来做 fee Indexer,主键应包含哪些 market 字段?

ch10-12 Predict 事件和未来 Indexer 需求

返回本章

先看市场问题

这里先从市场定义往下看。“Predict 事件和未来 Indexer 需求”服务的是 Predict 市场如何被定义、定价、mint、settle 或索引;不要把它读成普通现货订单簿的变体。

源码入口

  • packages/predict/sources/predict.move:mint、redeem、supply、withdraw、配置相关事件。
  • packages/predict/sources/oracle.move:spot/SVI/settlement 更新事件。
  • PREDICT_MIGRATION.md:Indexer/Server 仍处计划或阻塞状态的版本依据。

读市场参数

当前合约事件覆盖交易、LP、配置和 oracle 变化:PositionMintedPositionRedeemedSuppliedWithdrawnTradingPauseUpdatedPricingConfigUpdatedRiskConfigUpdatedOracleAskBoundsSetOracleAskBoundsClearedQuoteAssetEnabledQuoteAssetDisabledOracleFeedIdSetOracleBasisBoundsUpdatedOracleCreatedOracleActivatedOraclePricesUpdatedOracleSVIUpdatedOracleSettledFeeAccrued 等。

PREDICT_MIGRATION.md 把 Predict schema、indexer 和 server 放在 Phase 2,尚未完成。因此本章只能说事件具备未来 Indexer 的数据基础,不能说已有稳定 Predict API。应用如果现在基于源码快照开发,需要直接读对象和交易事件,或自建临时索引。

事件可以支持两类读模型:用户账本和市场风险。用户账本关注 manager、owner、range key、quantity、cost、payout、fee;市场风险关注 oracle、expiry、vault balance、MTM、max payout、PLP supply 和 limiter 状态。

在 Indexer 未完成前,应用不能依赖公共 Predict API 返回完整 PnL。可行的过渡方案是保存用户签名交易的 digest 与 dry run 摘要,再用 RPC 查询交易详情重建局部历史。

Predict 边界判断

  • 所有 Indexer 描述都用“未来需求”“可设计为”,不写成已上线能力。
  • 事件 schema 必须包含 package/network/oracle/expiry/lower/higher。
  • 前端缓存要能在 RPC 交易查询失败时显示“历史待同步”,不能覆盖链上余额。

动手检查

  • Position 事件和 Vault/PLP 事件分别服务哪类页面?
  • 没有稳定 Indexer 时,用户 PnL 可以从哪些数据源临时恢复?
  • 未来 Server API 和链上对象状态发生冲突时,应以哪个为准?

ch10-13 结合测试分析 mint、exercise、settle、withdraw

返回本章

先看市场问题

读“结合测试分析 mint、exercise、settle、withdraw”时先抓参数边界。Predict 的关键不是某个函数名,而是 oracle、expiry、strike、vault 和 payout 之间的关系。

源码入口

  • packages/predict/tests/*:fee reserve、pricing config、rate limiter、oracle cap 等当前测试。
  • packages/predict/sources/predict.movevault/vault.movepredict_manager.move:端到端交易路径对应实现。
  • packages/predict/simulations/src/runtime.tssim.ts:localnet 仿真中的 PTB 执行和 gas 汇总。

读市场参数

当前测试目录覆盖的是底层模块而不是完整端到端交易。tests/accounting/fee_reserve_tests.move 验证 fee split、默认比例、dust 留给 LP 和 zero fee;tests/config/pricing_config_tests.move 验证 fair price fee、min fee 和 utilization fee;tests/helper/rate_limiter_tests.move 验证提款限速、refill、deposit replenish 和配置错误;tests/helper/oracle_cap_tests.move 验证 oracle cap 注册、撤销和自撤销。

迁移文档中的 predict_tests.movepredict_manager_tests.movemarket_key_tests.move 和 cross validation tests 仍在计划中。写生产测试时必须补齐 create_predict -> enable_quote_asset -> create_oracle -> activate/update oracle -> create manager -> deposit -> mint -> live redeem -> settle -> settled redeem -> supply/withdraw 的完整路径。

测试阅读时要把“当前已有覆盖”和“应补覆盖”分开。fee、pricing、limiter、oracle cap 能证明局部约束,但不能自动证明所有 mint/redeem/settle/withdraw 组合都已完成生产级验证。

应用可以把测试断言转成 dry run 预检查:余额足够、manager owner 正确、oracle fresh、range key 合法、exposure 未超、limiter 可用、settlement price 已冻结。失败后把 Move abort 映射到具体用户动作。

Predict 边界判断

  • 测试描述不夸大为主网保障,只说明本书采用的 GitHub 源码快照覆盖面。
  • 每个交易路径至少列出成功断言和一个失败断言。
  • 仿真 gas 只作为 localnet 参考,不写成生产性能承诺。

动手检查

  • 现有测试覆盖哪些模块,缺少哪些端到端场景?
  • mint 失败和 withdraw 失败分别最应该 dry run 哪些条件?
  • 如何把 Move abort code 映射成用户能理解的提示?

ch10-14 已完成和待完成模块

返回本章

先看市场问题

这里先从市场定义往下看。“已完成和待完成模块”服务的是 Predict 市场如何被定义、定价、mint、settle 或索引;不要把它读成普通现货订单簿的变体。

源码入口

  • PREDICT_MIGRATION.md:PR 状态和 Phase 2 计划。
  • packages/predict/sources/*:本书采用的 GitHub 源码快照中的 Move 模块。
  • packages/predict/simulations/*:本地仿真 runtime、scenario 和可视化脚本。

读市场参数

PREDICT_MIGRATION.md,已完成:Oracle、constants/math、Vault、pricing config、risk config。迁移计划中待完成:Market Key、PredictManager、Predict Core、Registry、单元测试、集成测试、Predict schema、Predict Indexer、Predict Server、部署脚本、oracle services、Docker 和 CI。

按本书采用的 GitHub 源码快照,可阅读和本地开发环境试验:range_key.movepredict_manager.movepredict.moveregistry.moveoracle_config.movefee_reserve.move、仿真 runtime。书稿后续章节引用这些文件时,必须加上“本书采用的 GitHub 源码快照已有,迁移计划仍未全部标为完成”的限定。

可以把模块分成三层:迁移文档已合入的链上基础、工作树中可读可仿真的 Move/Simulation、尚未完成的 Indexer/Server/部署脚本/Oracle services。只有第一层能被写成已合入;第二层适合精读和本地实验;第三层只能写设计边界。

SDK 或服务配置建议包含 predictVersionStatus:例如 migration-pr1-pr2-merged-local-snapshotlocalnet-simulation-onlymainnet-unverified。缺少该字段时,服务应拒绝构造真实用户交易,只允许生成示例或 dry run 教学。

Predict 边界判断

  • 每次更新文档前先核对迁移文档和实际发布对象。
  • 代码示例默认 localnet/simulation,主网需要显式配置和版本证明。
  • 待完成项写成风险或后续工作,不写成当前能力。

动手检查

  • 哪些能力可以作为链上已合入基础讲解?
  • 哪些能力只能作为本书采用的 GitHub 源码快照或仿真使用?
  • 服务初始化缺少版本状态时应如何 fail fast?

ch11 DeepBook Predict 应用开发与仿真

本章目标

本章把 ch10 的源码对象翻译成应用开发路径:如何组织市场页、如何创建 market、如何创建或加载 PredictManager、如何构造 mint/redeem/supply/withdraw PTB,以及如何使用本地 simulation runtime 评估 gas、延迟和 LP 风险。

版本状态必须先说清楚:本章参考的是 packages/predict/simulations 与 GitHub 源码快照中的 packages/predict/sources/*PREDICT_MIGRATION.md 中 Predict Indexer、Predict Server、部署脚本和 oracle services 尚未完成;因此本章示例是本地开发和仿真骨架,不代表稳定主网 SDK 或稳定 API。

本章学习阶梯

  • L1 先设计 Predict 页面需要展示的信息。
  • L2 构造 mint、redeem、LP supply/withdraw 的交易或仿真路径。
  • L3 把仿真结果映射到 vault、oracle、pricing 和 risk 源码。
  • L4 做出预测市场 CLI 或 LP 收益曲线。

源码地图

小节目录

本章代码

  • book/ch11/code/s01-create-predict-market/:市场创建 PTB 顺序。
  • book/ch11/code/s02-mint-position/:用户创建 manager、deposit、mint position。
  • book/ch11/code/s03-supply-lp/:LP supply 与 withdraw 的交易顺序。
  • book/ch11/code/s04-settle-market/:oracle settlement 后的 settle 和 redeem 流程。
  • book/ch11/code/s05-simulation-report/:读取 results.json 生成报告。

Move 高阶穿插点

  • 仿真不是前端假数据,而是把 Move 中的 pricing、risk、vault 约束搬到链下预演。
  • Predict UI 要把资源状态翻译成人能理解的风险语言,但不能丢掉链上原始整数和 oracle 时间。
  • 交易前检查要覆盖暂停、quote asset 是否启用、oracle 是否 stale、vault 是否有足够流动性。

常见错误

  • 直接调用 predict::mint,但用户没有创建或充值 PredictManager
  • 前端把 fair price 当最终成交价,漏掉 fee 和 ask bounds。
  • 在 UI 中只显示 “UP/DOWN”,没有显示 expiry、oracle ID 和 settlement rule。
  • 把 localnet DUSDC mint 示例复制到生产环境。
  • 忽略 oracle staleness,导致用户在不可报价状态下反复签名失败。
  • 把 simulation localnet 结果写成主网性能承诺。

本章检查清单

  • 能从 owner 派生 PredictManager ID,并在不存在时创建。
  • 能构造 binary 和 range RangeKey
  • 能描述 create market 的 admin/operator 前置条件。
  • 能解释 run.sh 的 setup、sim、resume、analyze 阶段。
  • 能读懂 results_v2 的 gas 和 wallMs 字段。
  • 能在 UI 上区分交易者风险和 LP 风险。

进阶练习

  1. sim.ts 增加 redeem action,并在 results.json 中增加 redeem summary。
  2. 为预测市场 CLI 设计 abort code 映射表。
  3. 用一组自定义 CSV 比较不同 basis bounds 对仿真成功率和 gas 的影响。

ch11-01 Predict 应用的信息架构

返回本章

先跑通场景

Predict 应用不能照搬现货交易终端。现货页面通常围绕订单簿、下单表单和成交历史组织;Predict 页面还要解释到期时间、区间边界、oracle 状态、vault 风险、费用和 settlement。用户不是在“按价格买币”,而是在购买某个 oracle 在某个 expiry 上落入某个区间的收益权。

所以信息架构要先回答风险语言,而不是先排组件:这个市场引用哪个 oracle,什么时候到期,区间边界是什么,价格是否 stale,买入后最大亏损和最大收益是多少,LP vault 是否承担了过高 max payout。

源码入口

  • packages/predict/sources/predict.movepredict_manager.movevault/vault.move:页面对象和资金路径。
  • packages/predict/simulations/src/shared.ts:scenario/result schema 对信息架构的启发。
  • packages/predict/simulations/src/runtime.ts:应用服务需要的对象 ID、PTB 和执行摘要。

从仿真到产品页面

Predict 前端至少需要五类视图:市场列表、市场详情、交易面板、我的头寸、LP vault。

市场列表不是简单卡片流。它至少要展示 underlying、expiry、strike 或区间、UP/DOWN/Range、oracle 状态、当前 fair price、fee 和 ask price。市场详情进一步展开 oracle spot、forward、basis、SVI 更新时间、Lazer 更新时间、staleness 状态和 settlement 状态。

交易面板必须把 fair value、fee、all-in cost、最大收益、最大亏损、break-even 和成交后 vault 风险讲清楚。我的头寸按 RangeKey 聚合 quantity,并从事件或自建索引恢复成本。LP vault 页面展示 vault balance、vault value、total MTM、total max payout、available withdrawal、PLP supply 和提款限速状态。

这里的产品结构要绑定 localnet 仿真和可签名 PTB,而不是假设已经存在完整 Predict Server。所有对象 ID 来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本旁白:本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点,不能写成稳定产品依赖。

Predict 应用判断

  • 页面状态拆成 market catalog、oracle status、manager account、vault risk、simulation result 五块。
  • 每个 market tile 都显示 oracle ID、expiry、strike range、quote asset 和版本状态。
  • 不要依赖未完成 Predict Server;列表数据先来自配置、对象查询或本地仿真 state。
  • 交易按钮旁边必须显示 oracle freshness 和 settlement 状态,否则用户无法判断失败原因。

动手检查

  • 用户进入市场页前必须加载哪些 Predict 对象?
  • 哪些字段属于链上可信状态,哪些只是 UI 缓存或仿真结果?
  • 如果没有 Predict Indexer,市场列表和用户 PnL 各如何降级?

ch11-02 创建 Predict market

返回本章

先跑通场景

这一节先把场景落到可执行流程:读者需要看到对象从哪里来,PTB 如何构造,交易前检查什么,失败后如何回到源码定位。

源码入口

  • packages/predict/sources/registry.move:创建 Predict、Oracle、PredictManager 和配置入口。
  • packages/predict/sources/oracle_config.move:asset feed、basis bounds 和 strike grid。
  • packages/predict/simulations/src/sim.ts:localnet setup 中创建或加载对象的流程。

从仿真到交易

源码里 market 不是一个独立 Market 对象,而是 OracleSVI + OracleGrid + RangeKey 的组合。管理员或 oracle operator 的创建路径在 registry.move

  1. create_predict<Quote>(registry, admin_cap, currency, plp_treasury_cap, clock, ctx) 创建单例 Predict
  2. enable_quote_asset<Quote>(predict, admin_cap, currency) 允许某个 6 位 quote asset 进入 vault。
  3. create_oracle_cap(admin_cap, ctx) 给 operator 一个 OracleSVICap
  4. set_asset_feed_id(predict, admin_cap, "BTC", feed_id) 先绑定 Pyth Lazer feed。
  5. set_asset_basis_bounds(...) 可选,设置 per-asset basis circuit breaker。
  6. create_oracle(registry, predict, cap, underlying_asset, expiry, min_strike, tick_size, clock, ctx) 创建 oracle 并在 Predict 中初始化 strike grid。
  7. oracle::activate(oracle, cap, clock) 后才能报价。

应用创建市场前要检查 expiry 仍在未来、feed id 已配置、strike grid 满足 min strike 和 tick size 约束、basis bounds 不会让真实行情频繁触发 circuit breaker。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • create market PTB 按 registry、oracle config、oracle 创建、predict 激活的顺序组织。
  • 管理员 cap、operator 权限、feed id、basis bounds 和 strike grid 都从配置读取。
  • 本节只描述 localnet/setup 或当前发布入口,不写成稳定部署脚本。

动手检查

  • 创建 market 前必须准备哪些 cap 和配置对象?
  • feed id 或 basis bounds 缺失时 dry run 会暴露哪类问题?
  • 为什么 create market 示例不能直接等同于主网部署流程?

ch11-03 创建或加载 PredictManager

返回本章

先跑通场景

这一节从 Predict 的用户动作切入:先确认用户或 operator 要提交哪些对象和参数,再回到源码看市场状态如何被约束。

源码入口

  • packages/predict/sources/registry.move:manager 创建入口和派生 key。
  • packages/predict/sources/predict_manager.move:owner、BalanceManager、positions。
  • packages/predict/simulations/src/runtime.ts:对象 ID 查询、执行重试和状态保存。

从仿真到交易

用户交易前需要 PredictManager。仿真代码 packages/predict/simulations/src/runtime.tsderiveManagerId(owner, 0n) 使用 PredictManagerKey(address, u64) BCS 派生对象 ID,createManagerTx() 调用 registry::create_and_share_manager

前端流程应先按 owner 派生 manager ID 并查询对象;不存在时提示用户创建。创建后,quote coin 不直接传给 predict::mint,而是先 deposit 到 manager,交易时由合约从 manager 内部 BalanceManager 扣款。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • 先按 owner 派生 manager ID 并查询对象,不存在再创建。
  • 创建 manager 与 deposit 可以组合进同一 PTB,但后续 mint 必须引用正确 manager。
  • 缓存 manager 时同时保存 owner、network、package version 和 object version。

动手检查

  • PredictManagerKey(address, 0) 解决了什么查找问题?
  • owner 不匹配时交易应在前端、服务层还是 Move 层失败?
  • 用户已有 manager 但本地缓存丢失时如何恢复?

ch11-04 deposit quote 资产

返回本章

先跑通场景

deposit 不是“给页面充值”这么简单。Predict 的用户资金先进入 PredictManager,后续 mint、redeem、settle 都围绕 manager 内部余额和头寸表发生。钱包余额、manager 余额和 vault 余额是三层不同状态,任何一个混在一起都会导致金额展示和错误提示失真。

源码入口

  • packages/predict/sources/predict_manager.movedeposit<T> 与内部 BalanceManager。
  • packages/predict/sources/config/treasury_config.move:quote whitelist 和 decimals。
  • packages/predict/simulations/src/runtime.ts:coin 选择和 PTB 执行。

从仿真到交易

仿真里的 depositToManagerTx(managerId, amount) 先 mint DUSDC 测试币,再调用:

tx.moveCall({
  target: `${PACKAGE_ID}::predict_manager::deposit`,
  typeArguments: [DUSDC_TYPE],
  arguments: [tx.object(managerId), coin],
});

生产环境不会 mint 测试币,而是从钱包 coin 中 split 指定 amount。交易前检查包括 quote asset 是否被 treasury_config 接受、decimals 是否为 6、manager owner 是否等于当前地址、manager quote balance 是否足够支付 mint cost。

应用层要把 deposit 做成一个明确的账户动作:用户选择 coin、输入金额、钱包签名、链上确认、manager balance 刷新。只有 manager 余额刷新后,mint 面板才应该把这笔资金计入可用 quote。

本节仍然参考 localnet 仿真和当前源码,不依赖未完成的 Predict Server。对象 ID 来自配置或 setup state,提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

Predict 应用判断

  • deposit 前检查 quote coin type、coin balance、treasury whitelist 和 6 位 decimals。
  • PTB 参数明确 manager object、coin object/merged coin 和 sender。
  • 错误提示区分 coin 不足、quote asset 不支持、manager owner 错误。
  • UI 上分开展示 wallet quote balance、manager quote balance 和预计 mint cost。

动手检查

  • 为什么 deposit 到 PredictManager 后才适合调用 mint?
  • quote decimals 与 treasury config 不匹配会影响什么金额计算?
  • 如何在 UI 上区分钱包余额和 manager 内部余额?

ch11-05 mint binary position 的交易构造

返回本章

先跑通场景

这一节从 Predict 的用户动作切入:先确认用户或 operator 要提交哪些对象和参数,再回到源码看市场状态如何被约束。

源码入口

  • packages/predict/sources/predict.movemint<Quote>quote_mint_amounts
  • packages/predict/sources/market_key/range_key.move:UP/DOWN sentinel。
  • packages/predict/simulations/src/sim.ts:oracle refresh + mint 的 scenario 执行。

从仿真到交易

仿真 mintTx 把二元方向转换成 RangeKey:UP 使用 (strike, POS_INF],DOWN 使用 (NEG_INF, strike]。随后调用:

tx.moveCall({
  target: `${PACKAGE_ID}::predict::mint`,
  typeArguments: [DUSDC_TYPE],
  arguments: [
    tx.object(predictId),
    tx.object(managerId),
    tx.object(oracleId),
    key,
    tx.pure.u64(quantity),
    tx.object("0x6"),
  ],
});

交易前必须 dev inspect 或本地重算 quote,确认 all-in ask 没超过用户 slippage bound。当前 predict::mint 源码没有把用户自定义 max cost 作为参数,因此应用层要么用 PTB 前模拟,要么等待协议/SDK 增加保护参数后再面向生产。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • UP/DOWN 只改变 RangeKey sentinel,不改变 predict::mint 入口。
  • mint 前用 dev inspect 或本地 quote 计算 all-in ask,并与用户 slippage bound 比较。
  • dry run 摘要记录 fair price、fee、manager withdraw、vault payment 和 position increase。

动手检查

  • UP 和 DOWN 的 lower/higher sentinel 分别如何设置?
  • 当前 mint 没有 max cost 参数时,应用如何保护用户?
  • 二元 mint 失败最可能来自 oracle stale、余额不足还是 ask bound?

ch11-06 mint range position 的交易构造

返回本章

先跑通场景

这一节先把场景落到可执行流程:读者需要看到对象从哪里来,PTB 如何构造,交易前检查什么,失败后如何回到源码定位。

源码入口

  • packages/predict/sources/market_key/range_key.move:有限区间 key。
  • packages/predict/sources/vault/vault.movehelper/strike_matrix.move:range liability。
  • packages/predict/simulations/data/scenario_mar6_1000mints.csv:多价格路径输入。

从仿真到交易

range position 与 binary position 的差别只是 lower/higher 都是有限 strike。应用应提供 strike grid 选择器,确保 lower、higher 都按 oracle tick size 对齐。RangeKey::new 只检查 lower < higher 和不是全覆盖区间,grid 合法性还要结合 oracle config 和 mint 路径校验。

区间市场适合表达“到期价格落在某个范围内”的观点。收益是固定二元 payout,不随落点深浅变化;不要把它展示成线性价差收益。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • range mint 必须校验 lower < higher 且二者在 strike grid 内。
  • 有限区间的 UI 同时展示 payout 条件 (lower, higher] 和 settlement rule。
  • dry run 后检查 vault liability、max payout 和用户 position quantity。

动手检查

  • 有限 range position 与 UP/DOWN position 的 key 有什么不同?
  • 为什么不能构造 (-inf, +inf]
  • range 越宽时 vault risk 和 fee 可能如何变化?

ch11-07 LP supply 和 withdraw

返回本章

先跑通场景

这一节从 Predict 的用户动作切入:先确认用户或 operator 要提交哪些对象和参数,再回到源码看市场状态如何被约束。

源码入口

  • packages/predict/sources/predict.movesupply<Quote>withdraw<Quote>
  • packages/predict/sources/vault/plp.movevault/vault.move:PLP 和 vault value。
  • packages/predict/sources/helper/rate_limiter.move:提款限速。

从仿真到交易

LP supply 调用 predict::supply<Quote>(predict, coin, clock),返回 PLP coin。仿真中 supplyTx 使用测试 DUSDC mint 后供应,并把 PLP 转回当前地址。withdraw 调用 predict::withdraw<Quote>(predict, plp_coin, clock, ctx),源码会先检查所有 unsettled exposed oracle 的 MTM freshness,再按 PLP 占比计算可提 quote,并消耗 withdrawal limiter。

前端需要展示 available_withdrawal(predict, clock),并在提款失败时区分 MTM stale、rate limited、vault balance 不足和 quote asset 不存在。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • supply 前展示当前 vault value、total PLP、预估 shares 和 quote asset。
  • withdraw 前检查 MTM freshness、limiter available、PLP balance 和未结算 oracle。
  • 收益曲线同时拆分 LP fee 收益与 settlement liability。

动手检查

  • 首次 supply 和后续 supply 的 PLP 计算有什么差异?
  • 提款失败时如何区分 limiter、MTM stale 和 vault value 问题?
  • LP 页面为什么必须展示 max payout 而不只展示 APR?

ch11-08 oracle 更新和结算窗口

返回本章

先跑通场景

这一节先把场景落到可执行流程:读者需要看到对象从哪里来,PTB 如何构造,交易前检查什么,失败后如何回到源码定位。

源码入口

  • packages/predict/sources/oracle.move:spot/SVI 更新和 settlement。
  • packages/predict/sources/oracle_config.move:feed 和 bounds 配置。
  • packages/predict/simulations/src/sim.ts:仿真中更新时间和交易窗口。

从仿真到交易

仿真 setup 会先 set_asset_feed_id("BTC", 1),再把 BTC basis bounds 放宽到 10%,因为 CSV 的历史 spot 相邻变化可能超过默认 2%。随后 updateBasisTxoracle::update_pricesupdateSviTxoracle::update_svi

生产设计中,Pyth Lazer spot、operator basis 和 SVI 参数是不同频率的数据源。oracle.move 有 spot staleness、basis staleness、Lazer authoritative window 和 Lazer settlement authoritative window。前端不能只看最后价格,还要展示数据是否 stale、oracle 是否 active、是否 pending settlement 或 settled。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • oracle refresh、mint 和 settle 窗口都显示时间戳、staleness threshold 和 operator 来源。
  • 仿真可把 refresh + mint 放在 scenario 中,生产文档必须标注 Oracle service 未稳定完成。
  • settlement 后禁用 live quote,切换到 settled payout 领取路径。

动手检查

  • oracle stale 时用户反复签名失败的根因是什么?
  • settlement window 内哪些操作应被隐藏或提示高风险?
  • 为什么不能把仿真里的 oracle refresh 当作线上服务承诺?

ch11-09 settled market 的收益领取

返回本章

先跑通场景

这一节从 Predict 的用户动作切入:先确认用户或 operator 要提交哪些对象和参数,再回到源码看市场状态如何被约束。

源码入口

  • packages/predict/sources/predict.moveredeemredeem_permissionless
  • packages/predict/sources/market_key/range_key.move:settled payout。
  • packages/predict/sources/oracle.move:settlement price 冻结。

从仿真到交易

到期后,oracle settlement price 冻结。用户可调用 predict::redeem,如果 oracle 已 settled,源码走 settled path;也可由 keeper 调 redeem_permissionless,把 payout 存入用户 manager。结算 payout 使用 (lower, higher],刚好等于 higher strike 算命中,刚好等于 lower strike 不命中。

应用应为 settled position 提供 claim 操作,也应允许批量 claim。由于当前稳定 Indexer 尚未完成,批量列表需要读 manager position table 或维护自己的事件索引。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • claim PTB 调用 settled redeem 或 permissionless redeem,确认 oracle 已冻结 settlement price。
  • 批量领取列表来自 manager position table、交易历史或自建临时索引。
  • 领取结果检查 manager 余额增加、position 减少和 zero-fee 结算路径。

动手检查

  • settled redeem 为什么不应再使用 live fair price?
  • permissionless redeem 给 keeper 带来什么便利和边界?
  • 没有稳定 Indexer 时,批量 claim 列表如何生成?

ch11-10 前端展示风险和收益

返回本章

先跑通场景

这一节先把场景落到可执行流程:读者需要看到对象从哪里来,PTB 如何构造,交易前检查什么,失败后如何回到源码定位。

源码入口

  • packages/predict/sources/vault/vault.move:MTM、max payout、vault value。
  • packages/predict/sources/config/pricing_config.moverisk_config.move:ask/risk 边界。
  • packages/predict/simulations/visualize.py:风险与收益图表思路。

从仿真到交易

交易卡片至少展示:成本、fee、最大 payout、最大亏损、到期条件、oracle 更新时间、报价是否 stale、vault utilization、ask bounds。LP 卡片至少展示:vault value、total MTM、total max payout、PLP share、withdrawal limiter、未结算 oracle 数量。

错误处理不要只显示 Move abort code。应用应把常见错误映射成可操作提示,例如 owner 不匹配、余额不足、quote asset 未启用、oracle stale、trading paused、ask price out of bounds、exposure 超限、MTM stale、withdrawal budget 不足。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • 交易者视图展示 all-in ask、max loss、payout condition、oracle freshness。
  • LP 视图展示 vault value、MTM、max payout、PLP NAV、limiter。
  • 所有收益/风险图表标注 localnet simulation 或实际网络对象来源。

动手检查

  • 交易者和 LP 分别最关心哪三个风险字段?
  • oracle stale 与 vault exposure 超限在前端提示上有什么差异?
  • 为什么只展示 UP/DOWN 会误导用户理解市场?

ch11-11 仿真脚本如何构造市场路径

返回本章

先跑通场景

这一节从 Predict 的用户动作切入:先确认用户或 operator 要提交哪些对象和参数,再回到源码看市场状态如何被约束。

源码入口

  • packages/predict/simulations/run.sh:setup、sim、resume、analyze。
  • packages/predict/simulations/src/runtime.ts:client、PTB、retry、gas 汇总。
  • packages/predict/simulations/src/sim.ts:scenario 执行和结果写入。

从仿真到交易

sim.ts::setupSimulation 完成 localnet 初始状态:finalize DUSDC currency、create Predict、create OracleCap、set BTC feed id、放宽 basis bounds、create oracle、activate oracle、supply vault、create manager、deposit manager,并把 predictIdoracleIdoracleCapIdmanagerIdexpiry 写入 artifacts/state.json

executeScenario 读取 CSV。若连续三行是 update_prices -> update_svi -> mint,它会合成一个 refreshOracleAndMintTx,在一个 PTB 中更新 basis、更新 SVI、构造 RangeKey 并 mint。其他行则分别执行 update 或 mint。每个动作记录 wall time 和 gas。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • run.sh 阶段拆成 setup、sim、resume、analyze,并保留 artifacts state。
  • scenario CSV 行映射为 oracle refresh、mint 和结果记录,不写成通用撮合引擎。
  • gas 和 wallMs 汇总保存 action、digest、status、abort 信息。

动手检查

  • setup state 中哪些 object ID 会被后续 scenario 复用?
  • resume 如何避免重复发布或重复创建对象?
  • localnet gas/wallMs 能回答什么问题,不能回答什么问题?

ch11-12 仿真结果如何可视化

返回本章

先跑通场景

这一节先把场景落到可执行流程:读者需要看到对象从哪里来,PTB 如何构造,交易前检查什么,失败后如何回到源码定位。

源码入口

  • packages/predict/simulations/src/shared.ts:results schema。
  • packages/predict/simulations/visualize.py:图表生成。
  • packages/predict/simulations/data/scenario_mar6_1000mints.csv:输入场景。

从仿真到交易

shared.ts 定义 results_v2summary.totalTxssummary.byAction.{update_prices,update_svi,mint}mints[]。每个 action summary 包含 count、gas avg/min/max、wallMs avg/min/max;mint row 包含 wallMs、computationCost、storageCost、storageRebate、gasTotal。

运行方式:

cd deepbookv3
bash packages/predict/simulations/run.sh

已有 run 可恢复:

cd deepbookv3/packages/predict/simulations
bash run.sh --list
bash run.sh --resume <run-id> --sim
npm run analyze -- runs/<run-id>/artifacts/results.json

localnet 仿真输出适合比较 gas、延迟和 mint 执行数据,不提供 replay-derived trace profile。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • 图表至少覆盖成功率、gas 分布、wallMs、vault value、MTM、PLP NAV。
  • 读取 results.json 时保留失败 action 和 abort reason,不只画成功交易。
  • 报告标题和注释明确来自 simulation results,而不是主网监控。

动手检查

  • results.json 中哪些字段适合做 gas/latency 图?
  • LP 收益曲线需要哪些 vault 和 fee 字段?
  • 为什么仿真可视化不能当作生产 SLA?

ch11-13 Predict 与 Margin/Spot 组合产品的设计边界

返回本章

先跑通场景

这一节从 Predict 的用户动作切入:先确认用户或 operator 要提交哪些对象和参数,再回到源码看市场状态如何被约束。

源码入口

  • packages/predict/sources/predict.move:Predict 交易入口。
  • packages/deepbook/sources/pool.move:Spot 下单/swap 入口。
  • packages/deepbook_margin/sources/margin_manager.move:Margin 用户入口。

从仿真到交易

Spot 是现货撮合和 swap,Margin 是借贷、杠杆和清算,Predict 是到期二元/区间赔付。组合产品可以在应用层把 spot hedge、margin borrow 和 predict position 放在一个投资组合视图里,但不要把三者的风险混成一个合约能力。

例如“用 Margin 借 USDC 买 Predict UP”是应用层 PTB 或多步流程,不代表 Predict vault 接受 MarginPool 债权作为抵押。当前 Predict 的 quote 资产白名单由 treasury_config 控制,头寸由 PredictManager 管理。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • 组合产品先定义资金来源:Spot BalanceManager、MarginManager、PredictManager 不混用。
  • 跨产品 PTB 需要分别校验 oracle、risk ratio、vault exposure 和 object version。
  • 不把 Predict payout 当作 Margin 抵押品,除非协议明确支持。

动手检查

  • Predict vault、MarginPool、DeepBook pool 的职责差异是什么?
  • 一个组合 PTB 同时触碰 Spot/Margin/Predict 时,失败面会增加哪些?
  • 哪些组合只能作为产品设计,不能直接写成当前协议能力?

ch11-14 本章实战:预测市场 CLI

返回本章

先跑通场景

这一节先把场景落到可执行流程:读者需要看到对象从哪里来,PTB 如何构造,交易前检查什么,失败后如何回到源码定位。

源码入口

  • book/ch11/code/s01-create-predict-market/s04-settle-market/:CLI 命令骨架。
  • packages/predict/simulations/src/runtime.ts:PTB 构造和 dry run/执行摘要。
  • scripts/config/constants.ts:配置集中管理形态参考。

从仿真到交易

CLI 的最小命令:

  • predict market create:封装 feed id、basis bounds、oracle create、activate。
  • predict manager ensure:派生并创建 PredictManager
  • predict deposit:把 quote coin 存入 manager。
  • predict mint --up --strike --qty:构造 binary RangeKey 并 mint。
  • predict claim:对 settled position 调 redeem。
  • predict lp supply/withdraw:管理 PLP。

所有写交易命令都应支持 --dry-run,输出 PTB 摘要、预估 gas、可能 abort 和对象 ID。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • CLI 命令按 create-manager、deposit、mint、redeem、supply、withdraw、simulate 分组。
  • 所有命令输出 network、package/object IDs、dry run status、digest 和错误映射。
  • 默认配置指向 localnet/simulation,主网执行需要显式 --network 和版本确认。

动手检查

  • CLI 哪些命令需要用户签名,哪些只读?
  • mint 命令的最小参数集合是什么?
  • 为什么 CLI 应默认 dry run 而不是直接提交?

ch11-15 本章实战:仿真价格路径和 LP 收益曲线

返回本章

先跑通场景

这一节从 Predict 的用户动作切入:先确认用户或 operator 要提交哪些对象和参数,再回到源码看市场状态如何被约束。

源码入口

  • book/ch11/code/s05-simulation-report/:报告骨架。
  • packages/predict/simulations/src/shared.tsvisualize.py:results 与图表。
  • packages/predict/simulations/src/sim.ts:价格路径与 LP 状态变化。

从仿真到交易

基于现有 simulation runtime,可以扩展 CSV schema,加入 LP supply/withdraw 和 redeem action。结果报告除了 gas/latency,还应计算 vault balance、vault value、total MTM、total max payout、PLP NAV 和 fee reserve 增长。

不要把仿真结果当成收益承诺。SVI 参数、basis bounds、用户 mint 分布、oracle staleness 和 withdrawal limiter 都会显著改变 LP 曲线。

应用实现应把这一节绑定到 localnet 仿真和可签名 PTB,而不是绑定到未完成的 Predict Server。所有对象 ID 都来自配置或 setup state,交易提交前先 dry run,并把 gas、wallMs、Move abort 和对象变化写入结果摘要。

版本状态上,本章示例参考 packages/predict/simulations/* 与本地 packages/predict/sources/*PREDICT_MIGRATION.md 中未完成的 Indexer、Server、部署脚本和 Oracle services 只能作为后续集成点。

Predict 应用判断

  • 扩展 CSV schema 时明确 action、timestamp、spot、basis、quantity、lp action。
  • 报告同时输出 vault balance、vault value、MTM、max payout、PLP NAV 和 fee reserve。
  • 收益曲线标注路径假设和 localnet 限制。

动手检查

  • 价格路径如何影响 binary/range position 的 MTM?
  • LP 收益曲线中哪些变化来自 fee,哪些来自 liability?
  • 新增 redeem/withdraw action 后 results schema 要补哪些字段?

ch14 构建自己的 DeepBook 应用

本章目标

  • 把前面章节的协议、SDK 和 Indexer 能力组合成真实应用。
  • 设计 Spot、Margin、Predict、做市、套利和风控系统。
  • 建立从交易构造、数据查询、状态确认到生产监控的工程闭环。

本章学习阶梯

  • L1 先选应用类型:交易终端、做市机器人、Margin 面板、Predict 市场或数据服务。
  • L2 搭出最小信息架构和核心交易流程。
  • L3 接入 SDK、Indexer、Server、风控和错误状态机。
  • L5 做到可上线 MVP,能解释资金、安全和数据边界。

源码地图

小节目录

本章代码

  • code/s01-trading-terminal/:交易终端 MVP。
  • code/s02-market-maker-bot/:做市机器人骨架。
  • code/s03-arbitrage-bot/:套利机器人骨架。
  • code/s04-margin-dashboard/:Margin 仪表盘。
  • code/s05-predict-market-ui/:Predict 市场 UI。
  • code/s06-risk-monitor/:风险监控服务。

Move 高阶穿插点

  • 构建应用时,先定义你依赖的 Move 不变量,再设计页面、任务队列和后端服务。
  • 协议组合要画资源流图:哪些 coin 进入、哪些对象被借用、哪些资源必须归还或销毁。
  • 生产应用的高级能力不是功能堆叠,而是把失败路径、权限边界和资金安全讲清楚。

常见错误

  • 前端直接信任单一数据源,不做交易 digest 校验。
  • 把钱包余额当成 DeepBook 可用余额。
  • 做市机器人没有库存上限。
  • Margin UI 不展示清算阈值。
  • Predict UI 不标注 oracle 和结算规则。

本章检查清单

  • 能画出交易从 UI 到 PTB、链上、Indexer、UI 刷新的路径。
  • 能区分交易服务和数据服务。
  • 能说明机器人暂停条件。
  • 能说明每类应用的最小可用版本。

进阶练习

  • 为交易终端设计一个错误码到用户提示的映射表。
  • 为做市机器人设计一个库存偏移后的报价调整公式。
  • 为 Margin 仪表盘设计一个风险率颜色和告警规则。

ch14-01 应用构建路线

返回本章

先定义产品场景

构建 DeepBook 应用时,最容易犯的错误是从“我要调用哪个 move function”开始。真正的产品路线应该从用户动作和失败路径开始:用户想交易、做市、借贷、清算还是购买预测头寸;失败时是余额不足、权限不对、oracle stale、Indexer 延迟,还是对象版本过期。

源码入口

把系统拼起来

DeepBook 应用有四层:协议理解、SDK 交易、Indexer 数据、产品交互。协议层定义资金安全和状态机;SDK 层负责构造 PTB;Indexer 层负责历史数据和聚合;前端、机器人或后端 worker 负责用户操作、自动策略和风控降级。

应用路线要从“用户动作”倒推 PTB、数据查询和风控闭环。Spot 终端依赖 DeepBook 包与 Server 行情;Margin 还要引入风险率、借贷池、oracle 和清算;Predict 还要引入 expiry、range key、vault exposure 和 settlement。它们可以共享钱包、配置、dry run 和数据服务,但不能被塞进同一个通用交易表单。

出版社级应用章必须写出取舍:MVP 做什么,不做什么;哪些数据来自 public service,哪些必须自建;哪些交易允许用户直接签名,哪些应该由后端先生成 bytes;什么时候因为 checkpoint lag 或 oracle stale 暂停按钮。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。
  • 每个 MVP 都要写出降级模式:RPC 慢、Indexer 落后、oracle stale、钱包拒签时页面如何响应。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-02 Spot 交易终端信息架构

返回本章

先定义产品场景

产品小节先看上线闭环:交易、数据、风控、钱包和运维必须一起设计,不能只列功能按钮。

源码入口

把系统拼起来

Spot 终端至少包含:交易对选择、订单簿、最近成交、K 线、余额、下单区、当前委托、历史成交。对专业用户,订单簿和下单区优先级高于装饰性内容。

Spot 终端的信息架构应围绕池子选择、订单簿、下单、订单状态和成交历史组织。链上 PTB 负责真实下单,Server 提供行情和历史,钱包余额与 BalanceManager 可用余额要分开展示。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-03 订单簿和成交流组件

返回本章

先定义产品场景

产品小节先想用户路径和失败路径。DeepBook 应用的难点往往不在单个 move call,而在状态同步、错误解释和风险降级。

源码入口

把系统拼起来

订单簿展示 bid/ask 价格、数量和累计深度。成交流展示价格、数量、方向和时间。数据来自 Server API 或自建 Indexer。需要处理数据延迟、空数据、价格精度和刷新节流。

订单簿组件需要合并实时快照、增量刷新和成交流。成交流来自 order_fills,K 线来自 OHLCV 聚合;价格档位的展示则要有深度、精度和更新时间,不能只渲染最近成交价。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-04 下单表单和交易确认状态机

返回本章

先定义产品场景

产品小节先看上线闭环:交易、数据、风控、钱包和运维必须一起设计,不能只列功能按钮。

源码入口

把系统拼起来

交易状态机建议分为:编辑中、校验中、等待签名、已提交、链上确认、Indexer 确认、失败。不要只用钱包返回成功作为最终状态;交易确认和数据落库是两个阶段。

下单状态机至少包含编辑、模拟、钱包签名、提交、链上确认、Indexer 可见和失败恢复。每个状态都应记录交易 digest 或错误码,避免用户在 Indexer 延迟时重复提交同一意图。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-05 余额展示

返回本章

先定义产品场景

产品小节先想用户路径和失败路径。DeepBook 应用的难点往往不在单个 move call,而在状态同步、错误解释和风险降级。

源码入口

把系统拼起来

DeepBook 用户有钱包余额、BalanceManager 可用余额、订单锁定余额和 Margin 抵押/债务。UI 必须明确区分这些数值,否则用户会误判可交易额度。

余额展示要区分钱包 coin、BalanceManager 中可用余额、挂单占用和 Margin 抵押。前端可以用 Indexer 表做历史解释,但最终可用余额在发交易前仍要通过 SDK 或 dry run 校验。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-06 做市机器人

返回本章

先定义产品场景

产品小节先看上线闭环:交易、数据、风控、钱包和运维必须一起设计,不能只列功能按钮。

源码入口

把系统拼起来

做市机器人输入是中间价、订单簿深度、库存、手续费和目标 spread。输出是挂单、撤单和调仓。核心风险是库存偏移、Indexer 延迟、撤单失败和价格剧烈变化。

做市机器人由行情读取、报价模型、库存控制、下单执行和暂停条件组成。它必须把 /status、价差、库存上限、单笔失败率和 Gas 成本纳入循环,而不是只按固定价差挂买卖单。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-07 套利机器人与闪电贷

返回本章

先定义产品场景

产品小节先想用户路径和失败路径。DeepBook 应用的难点往往不在单个 move call,而在状态同步、错误解释和风险降级。

源码入口

把系统拼起来

套利机器人需要先计算完整路径利润,包括手续费、slippage、Gas 和失败概率。DeepBookV3 闪电贷适合在同一 PTB 内完成借出、交易、归还,但 hot potato 要求借出的资产必须在同一交易中等额归还。

套利机器人使用 DeepBook flash loan 时,借出、换路径、归还必须在同一 PTB 中完成。flashloans 只能用于统计闪电贷借出事件;它不是 Margin 债务,不能用来计算 loan_borrowedloan_repaid 的风险敞口。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-08 Margin 应用

返回本章

先定义产品场景

产品小节先看上线闭环:交易、数据、风控、钱包和运维必须一起设计,不能只列功能按钮。

源码入口

把系统拼起来

Margin 应用的核心不是下单,而是风险展示。用户必须看到抵押品、债务、风险比率、可借额度、利息和清算阈值。每次借款、交易、还款前都应 dry run。

Margin 应用要把抵押、借款、偿还、清算价和风险参数放在同一视图。用户看到的健康度应来自 MarginPool 参数、oracle 价格和债务事件流,不能只看钱包余额或最近成交价。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-09 Predict 应用

返回本章

先定义产品场景

产品小节先想用户路径和失败路径。DeepBook 应用的难点往往不在单个 move call,而在状态同步、错误解释和风险降级。

源码入口

把系统拼起来

Predict 应用要展示市场到期时间、oracle 价格源、赔率、最大损失、LP 风险和结算规则。对于还在迁移或测试阶段的能力,产品文案必须明确标注网络和版本状态。

Predict 应用的核心不是订单簿,而是市场定义、oracle 更新、下注/赎回和结算披露。UI 必须展示市场 key、价格来源、结算时间和费用,避免用户把预测份额当作普通现货仓位。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-10 后端交易服务

返回本章

先定义产品场景

产品小节先看上线闭环:交易、数据、风控、钱包和运维必须一起设计,不能只列功能按钮。

源码入口

把系统拼起来

后端交易服务可以帮助前端构造交易,但不应该托管用户私钥。服务端返回待签名 PTB 或交易参数,用户钱包签名提交。服务端负责配置校验、对象 ID 管理、dry run、错误码映射和日志。

后端交易服务适合封装报价、模拟、风控和交易模板,但不应代替用户签名权限。服务返回的是待签 PTB、仿真结果和风险提示;真正资产转移仍要由钱包或受控 signer 执行并记录 digest。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-11 数据服务

返回本章

先定义产品场景

产品小节先想用户路径和失败路径。DeepBook 应用的难点往往不在单个 move call,而在状态同步、错误解释和风险降级。

源码入口

把系统拼起来

数据服务从 DeepBook Server 或本地 PostgreSQL 读取数据,向前端输出更稳定的 DTO。它负责缓存、分页、限流和聚合,不负责资金状态修改。

数据服务负责把 Indexer 表转换成应用级 API。每个端点都要定义过滤条件、分页、缓存时间、错误码和指标;机器人使用的数据端点还要暴露数据新鲜度。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-12 风控系统

返回本章

先定义产品场景

产品小节先看上线闭环:交易、数据、风控、钱包和运维必须一起设计,不能只列功能按钮。

源码入口

把系统拼起来

风控系统要监控:RPC 可用性、Indexer lag、订单失败率、价格偏离、库存偏移、Margin 风险率、清算队列、数据库延迟。策略系统必须能在风控触发时停止发单。

风控系统要同时看市场风险、仓位风险和基础设施风险。Margin 清算、价格过期、checkpoint lag、机器人库存和 API 错误率都应进入同一套暂停与告警规则。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-13 组合结构化产品

返回本章

先定义产品场景

产品小节先想用户路径和失败路径。DeepBook 应用的难点往往不在单个 move call,而在状态同步、错误解释和风险降级。

源码入口

把系统拼起来

Spot、Margin、Predict 可以组合成更复杂产品,例如带止损的杠杆预测、Delta 对冲预测仓位、基于 DeepBook 流动性的收益产品。组合产品的难点是风险披露和状态同步,而不是简单地多调用几个函数。

结构化产品通常组合 Spot、Margin、Predict 或收益策略,产品层必须写清资金流和最坏情形。链上组合越复杂,链下披露、估值、赎回限制和审计记录越重要。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-14 上线前披露

返回本章

先定义产品场景

产品小节先看上线闭环:交易、数据、风控、钱包和运维必须一起设计,不能只列功能按钮。

源码入口

把系统拼起来

金融应用上线前至少说明:智能合约风险、预言机风险、流动性风险、清算风险、数据延迟风险、交易失败风险和网络拥堵风险。开发者不能把模拟收益当成确定收益。

上线前披露要覆盖协议风险、oracle、清算、费用、数据延迟和运维降级。披露不是营销文案,而是把用户可能亏损、交易失败或看到延迟数据的场景提前写清。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-15 专业交易终端 MVP

返回本章

先定义产品场景

产品小节先想用户路径和失败路径。DeepBook 应用的难点往往不在单个 move call,而在状态同步、错误解释和风险降级。

源码入口

把系统拼起来

第一版的边界要收紧:池子列表、订单簿、最近成交、BalanceManager、限价单、市价单、撤单、订单历史。先做交易闭环,再做高级图表。

专业交易终端 MVP 至少应完成池子选择、订单簿、下单、交易确认、历史成交和错误提示。MVP 可以牺牲高级图表,但不能缺少 digest 跟踪、Indexer 延迟提示和余额校验。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-16 做市机器人 MVP

返回本章

先定义产品场景

产品小节先看上线闭环:交易、数据、风控、钱包和运维必须一起设计,不能只列功能按钮。

源码入口

把系统拼起来

第一版的边界要收紧:读取中间价、生成双边报价、提交订单、定时撤单、库存限制、失败重试。第一版不要追求最优策略,先保证不会无限累积库存。

做市机器人由行情读取、报价模型、库存控制、下单执行和暂停条件组成。它必须把 /status、价差、库存上限、单笔失败率和 Gas 成本纳入循环,而不是只按固定价差挂买卖单。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-17 Margin 仪表盘 MVP

返回本章

先定义产品场景

Margin 仪表盘不是“借款历史列表”。它要回答用户最关心的四个问题:我抵押了什么,借了什么,当前风险率是多少,价格变化到哪里会触发清算。只要这四个问题有一个说不清,MVP 就还不能上线给真实用户。

源码入口

把系统拼起来

MVP 范围应包含:MarginManager、抵押品、债务、风险率、借款、还款、清算历史和 reduce-only 操作。所有写交易都应先 dry run,并在签名前展示交易后的风险变化。

Margin 仪表盘要优先展示抵押、债务、风险率、清算价和历史借还。loan_borrowedloan_repaid 是债务流水,flashloans 是 Spot 闪电贷记录,不是仓位数据,二者必须在 UI 和 API 命名上隔离。

数据层至少需要两类刷新:链上直接读取 manager/pool 风险状态,用于交易前判断;Indexer/Server 读取历史借还和清算事件,用于时间线和报表。实时风控不能只依赖落库事件。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。
  • 风险率展示要同时给出当前值、阈值、价格敏感性和数据更新时间。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch14-18 Predict 市场 MVP

返回本章

先定义产品场景

产品小节先看上线闭环:交易、数据、风控、钱包和运维必须一起设计,不能只列功能按钮。

源码入口

把系统拼起来

第一版的边界要收紧:市场列表、市场详情、价格源、mint position、用户头寸、结算状态、LP supply。测试网阶段重点验证状态机和用户理解,不追求复杂收益策略。

Predict 市场 MVP 应覆盖市场列表、详情、下注、赎回/结算状态和 oracle 披露。测试和 UI 都要验证市场关闭、价格源不可用、结算延迟和费用展示。

产品落地判断

  • 从用户动作出发写清 PTB 输入、type arguments、权限对象、签名者和预期 digest。
  • 前端状态要区分钱包签名、链上确认、Indexer 可见和 Server API 缓存刷新。
  • 机器人和后端服务必须有库存、价格、checkpoint lag、错误率和人工暂停阈值。
  • 上线前为费用、滑点、oracle、清算、数据延迟和失败重试准备明确披露。

动手检查

  • 用户完成本节场景时,最小 PTB、API 查询和 UI 状态分别是什么?
  • 失败时应展示权限、余额、价格、oracle、清算、RPC 还是 Indexer 延迟中的哪一类原因?
  • 这个应用 MVP 上线前还缺哪项测试、监控、暂停开关或风险披露?

ch15 测试、安全、部署与出版级交付

本章目标

最后一章把全书从“能读、能写、能跑”推进到“能交付”。DeepBook 应用涉及资金、共享对象、链上事件、Indexer、Server、SDK、钱包和运维系统;任何一层没有测试和证据,都会在生产环境变成不可解释的风险。

本章目标是建立一套完整的交付标准:Move 单元测试证明协议边界,SDK 和 dry run 测试证明交易构造,Indexer snapshot 证明读模型稳定,前端 E2E 证明用户路径可用,监控和故障演练证明系统可以在异常时降级,出版级清单证明书稿和代码可以被读者复现。

本章学习阶梯

  • L2 先建立测试金字塔:Move、SDK、Indexer、前端和 E2E。
  • L3 用源码不变量设计测试和安全清单。
  • L4 准备部署、升级、监控、故障演练和审计材料。
  • L5 把全书项目整理成出版级交付。

验收地图

证据类型主要入口验证什么
Move 测试packages/deepbook/testspackages/deepbook_margin/testspackages/predict/tests资源约束、权限、撮合、风险和结算边界。
Indexer 快照crates/indexer/tests/snapshot_tests.rs事件到数据库行的读模型合同。
SDK / PTB 测试book/ch12/code/ 与交易脚本对象 ID、type arguments、dry run、错误解析和钱包签名交接。
部署证据dockerbook/ch15/code/s05-deployment-compose/服务拓扑、健康检查、回滚和配置隔离。
书稿验收book/STYLE_GUIDE.mdBOOK_TODO.md、本章清单术语、源码路径、命令、风险披露和章节质量。

小节目录

本章代码

  • code/s01-move-test-template/:Move 测试模板。
  • code/s02-sdk-test-template/:SDK 测试模板。
  • code/s03-indexer-snapshot-template/:Indexer 快照测试模板。
  • code/s04-e2e-test-template/:端到端测试模板。
  • code/s05-deployment-compose/:部署 compose 模板。
  • code/s06-audit-checklist/:审计清单。

Move 高阶穿插点

  • 测试要覆盖 Move 单元测试、PTB dry run、SDK 参数测试、Indexer 重放和生产监控,不要只测 happy path。
  • 安全审查从资源开始:谁能创建、转移、借用、销毁、升级和暂停对象。
  • 出版级交付要求每个结论都有源码、测试或交易证据,不能只写经验判断。

常见错误

  • 只测成功路径。
  • 把 dry run 当作完整安全测试。
  • 忘记 Indexer 和 Server 的版本兼容。
  • 没有记录交易 digest,导致线上问题无法追踪。
  • 书稿代码无法运行或缺少依赖说明。
  • 书稿写成“功能介绍”,但没有给出可复现命令、失败路径和风险边界。

本章检查清单

  • 每类交易都有成功和失败测试。
  • 每个 API 有分页、限流和错误响应。
  • 每个生产服务有健康检查。
  • 每个章节代码示例有 README。
  • 全书术语和路径已统一。

进阶练习

  • 为 Spot 交易写一份测试矩阵。
  • 为 Margin 清算写一份威胁模型。
  • 为 Indexer 设计一次全量重放演练。

ch15-01 测试金字塔

返回本章

先定交付标准

这一节按上线视角读“测试金字塔”。如果某个判断不能被测试、监控或运行手册证明,它就还不是出版级和生产级内容。

源码入口

从测试到上线

DeepBook 开发测试分四层:Move 单测验证合约不变量,SDK 单测验证交易构造,Indexer 快照测试验证事件落库,端到端测试验证从 UI 到链上再回到数据层的闭环。

测试金字塔从 Move 单元测试开始,向上覆盖 SDK/PTB、Indexer snapshot、Server API 和前端端到端。越靠近底层越应该快且确定,越靠近应用越应覆盖真实用户路径和故障提示。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-02 Move 单元测试

返回本章

先定交付标准

这一节先看验收证据:测试、日志、监控、部署步骤、审计材料或发布前核对项必须能复现。

源码入口

从测试到上线

Move 测试应覆盖正常路径、边界条件和 expected failure。金融协议测试不能只验证成功交易,还要验证错误权限、错误资产、错误数量、版本错误和资金不足。

Move 单元测试负责证明对象权限、资金守恒、错误码和边界值。DeepBook、Margin、Predict 的测试应分别覆盖撮合、闪电贷归还、借贷清算和 oracle/定价规则。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-03 撮合和资金边界测试

返回本章

先定交付标准

这一节按上线视角读“撮合和资金边界测试”。如果某个判断不能被测试、监控或运行手册证明,它就还不是出版级和生产级内容。

源码入口

从测试到上线

撮合测试关注价格优先、时间优先、部分成交、完全成交、撤单和自成交边界。资金测试关注余额锁定、结算、费用、返佣和 Vault 余额守恒。

撮合和资金边界测试要验证部分成交、取消、余额占用、费用和借贷份额的守恒。失败路径同样重要,例如余额不足、价格越界、权限对象错误和清算阈值附近的舍入。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-04 Indexer snapshot tests

返回本章

先定交付标准

Indexer snapshot test 是数据系统的合约测试。Move 事件、Rust handler、Diesel model、PostgreSQL schema 和 Server API 中任何一层变动,都可能让前端看到不同字段或错误数值。Snapshot 的价值就是把这些变化显性化。

源码入口

从测试到上线

crates/indexer/tests/snapshot_tests.rs 使用真实 checkpoint fixture 验证 handler 输出。它能防止事件结构、字段映射和表结构变化后静默破坏 API。

Snapshot tests 固定的是读模型合同。新增事件或改字段时,要同时更新 checkpoint fixture、预期 snapshot、schema model,并检查 flashloans 与 Margin loan_borrowedloan_repaid 没有混表。对交易终端来说,这不是后端细节,而是用户历史、K 线、资金流水和风险面板的事实来源。

一套合格的 snapshot 测试还要记录 fixture 来源、checkpoint 编号、事件类型、预期表和关键字段含义。否则测试虽然能跑,后来的人也不知道它保护的是哪条业务路径。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。
  • 每个 snapshot 都要能解释它保护的业务查询,例如成交流、闪电贷历史、Margin 借款或清算列表。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-05 SDK 测试

返回本章

先定交付标准

这一节按上线视角读“SDK 测试”。如果某个判断不能被测试、监控或运行手册证明,它就还不是出版级和生产级内容。

源码入口

从测试到上线

SDK 测试分为交易构造测试和 dry run 测试。构造测试检查 PTB 目标函数、对象参数和类型参数;dry run 测试检查链上执行是否会 abort。

SDK 测试要覆盖 PTB 构造、type arguments、对象输入和 dry run 错误映射。它不只验证函数能调用,还要验证调用者拿到的 digest、返回对象和错误提示能驱动前端状态机。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-06 前端端到端测试

返回本章

先定交付标准

这一节先看验收证据:测试、日志、监控、部署步骤、审计材料或发布前核对项必须能复现。

源码入口

从测试到上线

端到端测试覆盖钱包连接、下单、签名、提交、确认、订单刷新、撤单和错误提示。交易状态机是重点,不是按钮是否能点击。

端到端测试应从用户路径出发:连接钱包、选择池子、模拟、签名、提交、确认、等待 Indexer 可见和错误恢复。测试环境要能注入 API 延迟、余额不足和 checkpoint lag。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-07 金融协议安全清单

返回本章

先定交付标准

这一节按上线视角读“金融协议安全清单”。如果某个判断不能被测试、监控或运行手册证明,它就还不是出版级和生产级内容。

源码入口

从测试到上线

必须检查:权限、capability 泄漏、资产类型混淆、精度错误、溢出、预言机过期、价格偏离、重复执行、状态不同步、Indexer 延迟和交易失败后的 UI 状态。

金融协议安全清单应覆盖资金守恒、价格源、权限、限速、清算、费用和紧急暂停。每一项都要能指向测试或监控证据,不能只停留在“已审查”的文本声明。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-08 对象权限风险

返回本章

先定交付标准

这一节先看验收证据:测试、日志、监控、部署步骤、审计材料或发布前核对项必须能复现。

源码入口

从测试到上线

Sui Move 中 capability 是权限边界。管理员 cap、maintainer cap、supplier cap、pause cap 和 trade proof 都必须明确拥有者、传递路径和失效方式。

对象权限风险来自 shared object、capability、BalanceManager 和 registry 的组合。测试要证明非授权账户不能修改池子配置、提取他人余额、篡改 oracle 或绕过 Margin 风控。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-09 版本升级

返回本章

先定交付标准

这一节按上线视角读“版本升级”。如果某个判断不能被测试、监控或运行手册证明,它就还不是出版级和生产级内容。

源码入口

从测试到上线

DeepBook 相关 package 有版本和注册表概念。升级时需要同时考虑 Move package、SDK 常量、Indexer handler、schema migration、Server API 和前端配置。

版本升级要同时管理 Move package、Indexer handler、数据库 schema、Server API 和前端 SDK。发布计划应写明兼容窗口、回滚方式、迁移耗时和 snapshot 更新策略。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-10 部署拓扑

返回本章

先定交付标准

这一节先看验收证据:测试、日志、监控、部署步骤、审计材料或发布前核对项必须能复现。

源码入口

从测试到上线

推荐生产拓扑:前端、交易构造服务、DeepBook Server、DeepBook Indexer、PostgreSQL、Prometheus、Grafana、日志系统。Indexer 与 Server 分开部署,便于独立扩缩容。

部署拓扑至少包含 Indexer、Server、PostgreSQL、RPC、metrics 和告警。Indexer 与 Server 不应共享同一权限配置;数据库迁移、备份和只读连接池也要拆开管理。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-11 日志、指标和 SLO

返回本章

先定交付标准

这一节按上线视角读“日志、指标和 SLO”。如果某个判断不能被测试、监控或运行手册证明,它就还不是出版级和生产级内容。

源码入口

从测试到上线

日志必须包含 digest、checkpoint、pool、balance manager、endpoint、错误码。SLO 至少覆盖 API 延迟、Indexer lag、交易提交成功率和数据刷新延迟。

SLO 应以用户可感知结果定义,例如 API P95、错误率、checkpoint lag、Margin 轮询成功率和交易确认耗时。日志必须带 digest、pool_id、balance_manager_id 或 checkpoint,方便从 UI 问题追到链上和数据库。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-12 故障演练

返回本章

先定交付标准

这一节先看验收证据:测试、日志、监控、部署步骤、审计材料或发布前核对项必须能复现。

源码入口

从测试到上线

演练场景:RPC 超时、Indexer 落后、数据库不可用、订单提交失败、价格源过期、Margin 风险率异常、前端缓存过期。每个场景都要有用户提示和策略降级。

故障演练要覆盖 RPC 延迟、Indexer 停止、数据库只读、Server 过载、oracle 过期和机器人误下单。演练结果应产出阈值、告警、降级文案和恢复步骤,而不是只确认服务能重启。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-13 审计准备

返回本章

先定交付标准

这一节按上线视角读“审计准备”。如果某个判断不能被测试、监控或运行手册证明,它就还不是出版级和生产级内容。

源码入口

从测试到上线

审计前准备:资产清单、权限清单、关键不变量、交易流程图、错误码表、测试覆盖报告、已知风险和不审范围。没有威胁模型的审计效率很低。

审计准备需要交付架构图、威胁模型、测试矩阵、已知限制、部署权限和变更记录。对审计方最有价值的是可复现证据:测试命令、fixture、migration、风险假设和未修复问题清单。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。

动手检查

  • 本节的验收证据是 Move 测试、SDK 测试、snapshot、E2E、指标还是部署日志?
  • 哪些失败路径、权限边界、迁移风险或运维故障还没有被覆盖?
  • 如果生产事故发生,能否用 digest、checkpoint、日志和监控在 10 分钟内定位责任层?

ch15-14 出版级书稿标准

返回本章

先定交付标准

出版级书稿不是“内容很多”的书稿,而是读者可以信任的书稿。对 DeepBook 这种资金协议来说,可信来自四件事:事实有来源,源码能定位,代码能运行,风险讲得清楚。任何一节缺一项,都应该继续打磨。

源码入口

  • book/ch13/README.md:本章验收和交付证据来源。
  • book/ch14/README.md:本章验收和交付证据来源。
  • book/ch15/README.md:本章验收和交付证据来源。
  • book/ch15/code/s06-audit-checklist/README.md:本章验收和交付证据来源。

从书稿到证据

每章要有清晰的读者问题、官方文档基线、源码引用、关键定义、状态路径、代码示例、常见错误和练习。术语必须统一:不要同一个概念在不同章节混用多个译名,也不要把 Spot 闪电贷、Margin 借贷和 Predict vault 风险混成一种“借款”。

出版级书稿要求路径、术语、命令和代码输出可复现。章节文档要能从 README 源码地图一路定位到实现文件,并说明示例代码的运行前提、预期输出和常见失败。

写作上要避免两种极端:只复述官方文档,读者学不到源码;只贴源码片段,读者不知道为什么重要。合格的段落应当有一个明确问题、一个源码依据和一个工程判断。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。
  • 每章至少有一处把官方文档、源码定义和应用实现三者连起来的“硬段落”。

动手检查

  • 本章有哪些结论来自官方文档,哪些来自源码,哪些来自本书推导?
  • 代码示例能否在干净环境里按 README 跑通?
  • 读者能否从本章定位一个失败交易的 Move 入口、dry run error、event 和 indexer 行?

ch15-15 每章验收标准

返回本章

先定交付标准

每章验收标准要像工程 checklist 一样可执行,而不是像编辑口号。读者翻完一章后,应该能用源码、命令、截图、事件或测试证明自己真的掌握了这一章,而不是只觉得“看懂了”。

源码入口

  • book/ch13/README.md:本章验收和交付证据来源。
  • book/ch14/README.md:本章验收和交付证据来源。
  • book/ch15/README.md:本章验收和交付证据来源。
  • book/ch15/code/s01-move-test-template/README.md:本章验收和交付证据来源。

从章节到验收证据

章节验收包括五类证据:概念能复述,源码能定位,代码能运行,失败能解释,风险能披露。只满足前三项还不够,因为 DeepBook 应用最常见的问题往往出现在失败路径和数据延迟上。

每章验收标准应同时检查概念、源码定位、运行步骤和风险说明。对 ch13 要能跑通 Indexer/Server 路径,对 ch14 要能演示一个应用闭环,对 ch15 要能证明测试、安全和部署证据完整。

章节作者在交稿前至少要问一次:这一章有没有仍然像“介绍文档”的小节?有没有只给链接、不解释字段的源码引用?有没有用“未来会支持”替代当前网络状态?这些问题不解决,就不能算出版社级别。

交付判断

  • 每个测试或部署结论都要能回到具体命令、fixture、日志、指标或审计材料。
  • 同时覆盖成功路径、失败路径、边界值、权限错误和数据延迟。
  • 升级、迁移和部署步骤要写明回滚点、兼容窗口、健康检查和负责人。
  • 涉及资金安全时,把链上约束、SDK 构造、Indexer 读模型和前端提示分层验证。
  • 对短小节额外检查:是否有真实场景、源码依据和读者可执行任务。

动手检查

  • 这一章最关键的三条源码路径是什么?
  • 这一章的代码示例在哪里运行,预期输出是什么?
  • 这一章最容易误导读者的产品边界或网络状态是什么?

ch15-16 全书项目 README

返回本章

先定交付标准

这一节先看验收证据:测试、日志、监控、部署步骤、审计材料或发布前核对项必须能复现。

交付入口

README 应该解决什么

全书项目 README 不是封面文案,而是读者的第一份运行手册。它至少要回答五个问题:

问题README 中的交付方式
这本书按什么顺序读给出六阶路线,并说明哪些章节可以跳读。
源码以哪个版本为准写明 DeepBook GitHub 快照、官方文档日期、sandbox 仓库入口。
本地如何跑列出 mdbook build、sandbox、SDK、Indexer、Move 示例的入口命令。
示例状态如何判断用“已实现、可运行、蓝图、待补齐”标注每个代码目录。
出错如何定位给出 digest、checkpoint、日志、RPC、Server、PostgreSQL 的排查顺序。

一个合格的 README 应避免“请进入对应章节查看”这种空转句。读者第一次打开仓库时,应该能在 5 分钟内知道自己要安装什么、先运行什么、失败后看哪里。

推荐结构

1. 书籍定位和阅读路线
2. 事实基线:官方文档、deepbookv3 源码、deepbook-sandbox
3. 环境要求:Sui CLI、Node/pnpm、Rust、Docker、PostgreSQL
4. 快速预览:mdBook 构建和本地预览
5. 本地全栈:DeepBook Sandbox
6. 章节代码运行顺序
7. 常见问题:RPC、gas、package ID、Indexer lag、Predict 版本
8. 出版和审计状态

验收标准

  • README 中每条命令都能在对应目录执行,不能只写概念。
  • 每个环境变量说明来源、示例值和是否允许提交到仓库。
  • 每个外部链接注明用途:官方事实、源码快照、sandbox 工具或参考文档。
  • 对未完成代码目录显式标注“蓝图”,不能伪装成可运行示例。

交付检查

  • 新读者能否只凭 README 构建书稿并进入 ch16 sandbox?
  • SDK、Indexer、Move 示例是否有清晰的依赖和运行边界?
  • README 是否把当前仍未完成的代码 TODO 讲清楚,而不是隐藏起来?

ch15-17 代码运行手册

返回本章

先定交付标准

这一节按上线视角读“代码运行手册”。如果某个判断不能被测试、监控或运行手册证明,它就还不是出版级和生产级内容。

运行入口

运行手册的固定格式

每个代码目录都应该按同一套证据组织,而不是只列“模块”。推荐格式:

目标:这个示例验证哪条交易、数据或运维路径。
前置条件:Sui CLI / Node / Rust / Docker / PostgreSQL / sandbox。
环境变量:每个变量的来源、示例值、是否敏感。
启动命令:最小命令,不混入解释。
验证命令:RPC、digest、API、SQL、日志或页面检查。
停止/清理:如何回滚状态,哪些数据会丢失。
失败排查:按链上、SDK、Indexer、Server、UI 分层定位。

这套格式的意义是让读者在失败时有证据可追,而不是只能重跑全部步骤。DeepBook 应用涉及资金和共享对象,运行手册必须记录 transaction digest、checkpoint、object ID 和关键日志。

分层运行顺序

  1. 先跑 mdbook build,确认书稿导航和链接可生成。
  2. 再跑 ch16 sandbox,获得 localnet、package ID、pool ID 和 faucet。
  3. 跑 ch12 SDK 初始化,确认 RPC 与 manifest 可读取。
  4. 跑 ch13 Indexer/Server 示例,确认读模型能跟上 checkpoint。
  5. 跑 ch14 应用或机器人示例,验证交易构造、状态刷新和风险熔断。
  6. 最后跑 ch15 测试模板,把成功和失败证据固化。

不要一开始就跑完整产品。越靠近用户界面的示例,越依赖前面的链上和数据层证据。

交付检查

  • 每个示例是否有“启动”和“验证”两类命令?
  • 失败时是否能从 digest、checkpoint、日志或 SQL 中定位责任层?
  • 清理命令会不会删除链状态、数据库或生成配置?README 是否提前说明?

ch15-18 附录

返回本章

先定交付标准

这一节先看验收证据:测试、日志、监控、部署步骤、审计材料或发布前核对项必须能复现。

附录入口

附录应该怎么用

附录服务查阅,不承载新概念。正文中第一次讲清楚对象、资金路径和风险边界;附录负责让读者快速反查名称、路径、字段和命令。

建议最终拆成九类:

附录内容
A 函数索引place_limit_orderborrow_flashloan_base、Margin/Predict 入口等。
B 事件索引订单、成交、余额、闪电贷、Margin 借还、Predict mint/settle。
C 结构体索引PoolBookOrderBalanceManagerVaultMarginManager
D 错误码索引abort code、触发条件、用户提示、dry run 定位方式。
E 表结构索引Indexer 表、主键、查询模式、checkpoint 字段。
F SDK 片段初始化、BalanceManager、下单、撤单、dry run、错误解析。
G 命令速查mdbooksui movepnpmdocker composepsql
H 术语中英对照pool、vault、fill、rebate、checkpoint、hot potato。
I 事实核对清单官方文档、源码快照、sandbox README、网络状态和版本边界。

收录规则

  • 只收正文已经解释过的概念,不在附录里第一次引入关键机制。
  • 每个函数或事件都标注源码路径、章节来源和应用侧用途。
  • 表结构要写主键、常用过滤条件和一致性风险,不能只列字段。
  • 错误码要写用户看到什么、开发者查什么、是否可重试。

交付检查

  • 读者能否从一个错误码反查到源码、dry run 和用户提示?
  • 读者能否从一个事件名反查到 Indexer 表和 Server API?
  • 附录是否区分 DeepBookV3、Margin、Predict、sandbox 和本书示例?

ch16 DeepBook Sandbox 本地开发环境

DeepBook Sandbox 不是另一个“示例项目”。它更像本书前面十五章的本地实验室:一条命令拉起 Sui localnet、发布 DeepBook V3、部署 Margin、接上 oracle、跑起 market maker、启动 indexer/server、打开 dashboard,然后让读者在可重置的环境里验证交易、余额、事件、REST API 和自定义 Move 合约。

这章放在全书后段,是因为它需要读者已经理解 DeepBook 的几条主线:链上对象负责资金和规则,SDK 负责交易构造,Indexer/Server 负责读模型,Dashboard 负责把状态暴露给人。到了 sandbox 这里,这些能力第一次同时出现在一个本地拓扑里。

本章目标

  • 理解 MystenLabs/deepbook-sandbox 的定位:降低 DeepBook V3 本地开发、演示、集成和自定义合约实验的摩擦。
  • pnpm deploy-all 建立可复现的本地 DeepBook 全栈环境。
  • 读懂 localnet、PostgreSQL、Indexer、DeepBook Server、Faucet、Oracle、Market Maker、Dashboard 的职责边界。
  • 学会把自定义 Move package 依赖到本地已发布的 DeepBook package,并用 Pub.localnet.toml 解析地址。
  • 建立一套从 dashboard 到命令行、从合约到数据服务的日常调试流程。

本章学习阶梯

  • L2 把 sandbox 当成一套可重置的本地环境:先跑起来,再看服务是否健康。
  • L3 从 dashboard 反推交易流:BalanceManager 创建、充值、下单、成交、事件、读模型。
  • L4 进入自定义 Move 合约:依赖本地发布的 DeepBook、编译、发布、调用、失败排查。
  • L5 把 sandbox 变成产品开发内环:SDK 调试、Indexer 校验、机器人测试、风险演练和上线前预演。

官方仓库基线

能力本章如何使用
Sui Localnet提供私有链、RPC 和原生 faucet,避免污染测试网状态。
DeepBook V3 contracts在本地按依赖顺序发布 token、deepbook、pyth、usdc、margin 和 liquidation 相关 package。
PostgreSQL + Indexer把 checkpoint 和事件写入数据库,支撑 DeepBook Server 查询。
DeepBook Server给 dashboard、脚本和应用提供 REST 读接口。
Oracle Service更新本地 Pyth price object,让 market maker 和 Margin 场景有价格输入。
Market Maker持续挂 POST_ONLY 网格单,让本地池子有可交易深度。
Faucet给测试地址发 SUI、DEEP、USDC。
Dashboard用页面检查服务健康、池子、部署地址、faucet、交易动作和 SDK 片段。
Example Contract提供依赖 DeepBook 的自定义 Move 合约模板。

小节目录

本章代码

  • code/s01-sandbox-runbook/:从 clone 到 teardown 的本地运行手册。
  • code/s02-dashboard-checks/:Dashboard 页面验收清单。
  • code/s03-custom-contract-template/:自定义 Move 合约模板使用说明。
  • code/s04-service-health-checks/:服务健康检查、端口和排障命令。

Move 高阶穿插点

Sandbox 最值得学的不是 Docker,而是“链上依赖如何被本地化”。Pub.localnet.toml 记录本地链上已经发布的 package 地址,自定义合约通过 --pubfile-path 指向它,就能把 deepbooktokendeepbook_margin 这些依赖解析到真实 localnet 对象上。这个动作让 Move 的编译期依赖、发布地址、运行期 shared object 三件事连到一起,是读懂 Sui 应用开发内环的关键。

常见错误

  • 没有用 --recurse-submodules 克隆,导致 DeepBook 源码子模块缺失。
  • 直接跑 docker compose,绕过 pnpm deploy-all,结果 .env、package ID 或 key 没有被正确生成。
  • 忘记首次启动会编译 Rust indexer/server,把长时间构建误判为失败。
  • pnpm down 当作普通停止命令,忽视它会清理链状态和部分自动生成配置。
  • 自定义合约没有先执行 pnpm deploy-all.external-packages/ 尚未生成,依赖无法解析。
  • 用 dashboard 的读模型判断交易最终性,却没有核对 transaction digest、checkpoint 和链上对象。

本章检查清单

  • 能完整说出 pnpm deploy-all 发布了哪些 package、启动了哪些服务。
  • 能解释 dashboard 里的 Trading、Faucet、Deployment 页面分别验证什么。
  • 能用 faucet 给测试地址发 SUI、DEEP、USDC。
  • 能复制 example_contract,添加 localnet 环境并用 test-publish 发布。
  • 能在重置前保存 package ID、pool ID、oracle object 和测试 digest。
  • 能说明 sandbox 与生产部署的差异。

进阶练习

  1. 先用 dashboard 创建 BalanceManager、充值 DEEP/SUI,再用命令行或 SDK 查询同一个对象。
  2. 修改 market maker 参数,让订单簿深度变化,然后观察 dashboard 与 DeepBook Server 是否一致。
  3. 写一个最小 Move wrapper,读取或组合 DeepBook 的交易对象,再发布到 localnet。
  4. pnpm down 重置后重新部署,比较两次 deployments/localnet.json 的地址差异。

ch16-01 Sandbox 的定位

返回本章

DeepBook 的学习有一个很现实的断层:只读 Move 源码,很难感受到交易系统的完整形态;直接接主网或测试网,又会被地址、gas、RPC、Indexer 延迟、测试资金和环境污染拖住。DeepBook Sandbox 填补的是这个断层。它把协议、服务、数据、页面和测试资产放进一个本地、可重置、可观察的环境里。

这也是它和本书其他代码目录的区别。前面的章节更像“局部实验”:一次下单、一个 BalanceManager、一个 handler、一段 SDK 封装。Sandbox 是“全链路实验”:同一个动作会同时经过 localnet、Move package、shared object、Indexer、PostgreSQL、Server、Dashboard 和日志系统。

它适合解决什么问题

场景Sandbox 的价值
第一次集成 DeepBook不需要先整理主网 package ID 和测试资产,先在本地跑通交易流。
SDK 调试可以反复构造 PTB、dry run、提交交易,并立即清理状态重来。
Indexer/Server 调试本地 checkpoint 和事件稳定可控,适合验证读模型。
自定义 Move 合约本地已经发布 DeepBook 依赖,可以测试 wrapper、组合交易和权限边界。
演示和培训Dashboard 能直接展示服务健康、池子、faucet、部署地址和交易动作。

它不替代什么

Sandbox 不替代测试网验收,也不替代生产部署。它默认是本地私有链,地址会变,资产是测试资产,oracle 和 market maker 是为开发便利服务的模拟组件。用它验证逻辑和工程路径没有问题,用它证明生产安全就不够。

真正的边界应该这样划:

  • 用 Sandbox 验证交易构造、合约依赖、事件读模型和应用交互。
  • 用测试网验证外部 RPC、钱包、网络延迟、真实对象地址和升级流程。
  • 用生产环境验证监控、告警、SLO、权限管理、备份和事故响应。

Move 开发提醒

Move 合约开发最容易忽略“地址来自哪里”。在本地 sandbox 中,DeepBook 不是一个抽象依赖,而是已经发布到 localnet 的 package。自定义合约依赖 deepbook 时,编译器需要本地源码路径,发布时还需要 localnet 上已经存在的 package 地址。Pub.localnet.toml 正是把这两件事连起来的文件。

读这一章时,不要把 Docker 当主角。真正的主角是:一个 Move package 如何从源码、依赖、发布地址、shared object、事件,再进入应用读模型。

本节验收

  • 能解释为什么本书需要一个全栈 sandbox 章。
  • 能区分 sandbox、测试网、生产环境的职责。
  • 能说出 sandbox 对 Move 合约依赖解析和 DeepBook 应用开发的意义。

ch16-02 一条命令拉起本地栈

返回本章

DeepBook Sandbox 的开发入口非常克制:克隆仓库、安装依赖、复制 .env 示例,然后执行 pnpm deploy-all。第一轮会慢,因为 Docker 镜像、Rust indexer/server 构建、本地 Move package 发布都要完成;第二轮开始,缓存会让反馈快很多。

前置条件

官方仓库当前要求的本地工具包括:

工具要求
Docker Desktop需要包含 docker compose,建议给 Docker 至少 8 GB 内存。
Node.js18 或更高版本。
pnpm用于安装 sandbox 工作区依赖和运行脚本。
Sui CLIREADME 建议 1.63.21.64.1 区间,并用 sui --version 核对。

这里不要偷懒。Sandbox 是多服务环境,Docker 内存不足、Sui CLI 版本不匹配、没有递归拉取子模块,都会表现成“部署失败”,但根因并不在 DeepBook 合约。

最小启动路径

git clone --recurse-submodules https://github.com/MystenLabs/deepbook-sandbox.git
cd deepbook-sandbox/sandbox
pnpm install
cp .env.example .env
pnpm deploy-all

如果只是想快速体验,并接受使用预构建镜像,可以使用:

pnpm deploy-all --quick

看到 DeepBook Sandbox Ready! 之后,本地环境已经具备几类能力:Sui RPC、Sui faucet、DeepBook faucet、oracle status、market maker health、DeepBook REST API 和 dashboard。

第一次启动要观察什么

不要只盯着 dashboard 页面是否打开。更可靠的启动检查是按依赖顺序看:

  1. docker compose ps:容器是否都处于健康或运行状态。
  2. curl http://localhost:9010/:oracle 是否能返回状态和价格。
  3. curl http://localhost:3001/health:market maker 是否已经开始维护订单。
  4. curl http://localhost:9009/manifest:是否能看到部署后的 package ID、pool ID、oracle object。
  5. http://localhost:5173:dashboard 是否能代理和展示服务状态。

这里的顺序很重要。Dashboard 是观察面,不是根因面。页面空白可能是前端问题,也可能是服务未就绪、proxy 失败、faucet 未启动、manifest 没生成,甚至是 localnet 没有起来。

deploy-all 在做什么

可以把 pnpm deploy-all 看成一个开发编排器:

  • 启动 Sui localnet 和 PostgreSQL。
  • 等待 RPC 与 faucet 可用。
  • 导入或生成本地 key,配置 Sui CLI 的 localnet 环境。
  • 发布 DEEP token、DeepBook、Pyth、USDC、Margin 和 liquidation 相关 package。
  • 写入 .envdeployments/localnet.jsonPub.localnet.toml
  • 启动 indexer、server、sandbox API、oracle、market maker 和 dashboard。
  • 创建本地池子,并给 market maker 准备 BalanceManager 和初始资金。

这条流水线解释了为什么不要绕过它直接执行 docker compose up。容器起来不等于 DeepBook 可用,package ID、object ID、oracle object、pool ID 和 pubfile 都必须被正确写入。

停止和清理

pnpm down

这个命令适合结束当天实验或重新开始。执行前先保存需要保留的地址、digest、日志和 manifest。默认情况下,本地链状态会被清掉,重新部署后 package ID 和 object ID 可能变化。

本节验收

  • 能独立完成首次启动并解释第一轮为什么较慢。
  • 能说出 pnpm deploy-all 和普通 docker compose up 的差异。
  • 能在 dashboard 之前用命令行判断核心服务是否可用。

ch16-03 服务拓扑和端口

返回本章

Sandbox 的服务很多,但拓扑并不混乱。先把它拆成三层:链上执行层、链下读模型层、开发体验层。链上执行层负责 localnet 和 Move package;链下读模型层负责 checkpoint、PostgreSQL、REST API;开发体验层负责 faucet、oracle、market maker 和 dashboard。

flowchart LR
  Dashboard["Dashboard :5173"] --> RPC["Sui RPC :9000"]
  Dashboard --> Faucet["DeepBook Faucet :9009"]
  Dashboard --> Oracle["Oracle Service :9010"]
  Dashboard --> MM["Market Maker :3001"]
  Dashboard --> Server["DeepBook Server :9008"]
  Localnet["Sui Localnet :9000/:9123"] --> Indexer["Indexer :9184"]
  Indexer --> Postgres["PostgreSQL :5432"]
  Server --> Postgres
  Oracle --> Localnet
  MM --> Localnet
  Faucet --> Localnet

端口速查

服务默认端口主要用途
Dashboard5173浏览器入口,查看健康状态、交易、faucet、部署地址。
Sui RPC9000本地链 RPC,SDK 和 Sui CLI 的主要入口。
Sui Faucet9123Sui localnet 自带 faucet。
DeepBook Faucet9009发 SUI、DEEP、USDC,并暴露 deployment manifest。
DeepBook Server9008基于 indexer/PostgreSQL 的 DeepBook REST API。
Oracle Service9010查询 oracle 状态,服务会定期更新 price object。
Market Maker3001查询做市服务健康状态。
Market Maker Metrics9091Prometheus metrics。
Indexer9184读取 checkpoint 并写入 PostgreSQL。
PostgreSQL5432保存 indexer 写入的读模型。

请求如何流动

Dashboard 并不直接“拥有”交易能力。它是一个代理和操作台:

  • /api/sui 代理到本地 Sui RPC。
  • /api/oracle 代理到 oracle service。
  • /api/mm 代理到 market maker。
  • /api/faucet 代理到 faucet service。
  • /api/deepbook 代理到 DeepBook Server。

理解这一点后,排障会简单很多。Dashboard 的 Trading 页面失败,不代表 DeepBook 合约失败;可能是 faucet 没发币、wallet 没切 localnet、RPC 不通、Indexer 滞后、Server 查询不到,或者 PTB 构造本身 abort。

Move 与数据层的边界

Move 合约只负责交易规则和资源安全。交易是否展示在页面上,取决于事件被 indexer 读到、写入 PostgreSQL,再被 server 查询出来。对用户而言这是一个页面状态;对工程师而言,这是至少三层系统之间的一致性问题。

所以本地调试要同时保留三类证据:

  • 链上证据:transaction digest、object ID、event。
  • 读模型证据:checkpoint、PostgreSQL 行、server response。
  • UI 证据:dashboard 页面、浏览器请求、用户操作路径。

本节验收

  • 能画出 localnet、indexer、server、dashboard 的依赖关系。
  • 能根据端口判断哪个服务可能出错。
  • 能解释为什么 UI 展示不是链上状态的唯一证据。

ch16-04 Dashboard 工作流

返回本章

Dashboard 的价值不是“有个页面好看”,而是把 DeepBook 开发中最容易分散的状态集中起来。它让读者不用一上来就写 SDK、SQL、curl 和钱包脚本,也能看到服务健康、部署地址、faucet、订单簿、BalanceManager 和交易动作。

五个页面怎么读

页面主要回答的问题
Healthlocalnet、indexer、oracle、market maker、faucet 等服务是否可用。
Market Maker本地池子是否有双边报价,网格单是否在持续重平衡。
Trading钱包能否连接 localnet,是否能创建 BalanceManager、充值、下单。
Faucet测试地址能否拿到 SUI、DEEP、USDC。
Deployment当前 localnet 的 package ID、pool ID、oracle object 和部署清单是什么。

建议第一次进入 dashboard 时,不要直接下单。先走一遍观察路径:

  1. Health 页面确认服务可用。
  2. Deployment 页面保存当前 package ID 和 pool ID。
  3. Faucet 页面给测试钱包发资产。
  4. Trading 页面创建 BalanceManager 并充值。
  5. Market Maker 页面观察订单簿是否有深度。
  6. 再回到 Trading 页面执行限价单或市价单。

Trading 页背后的 Move 动作

第一次连接钱包时,页面会引导创建 BalanceManager。这个按钮背后不是一个普通前端状态,而是一组 PTB 动作:创建 balance_manager::new,注册 BalanceManager,并把对象共享出来。这样后续交易才能通过注册表发现和复用同一个交易账户。

这正好呼应本书前面反复强调的一个原则:DeepBook 不是直接拿钱包余额下单,而是通过 BalanceManager 管理可用余额、锁定余额和交易权限。Dashboard 把这个原则变成了可点击流程。

Dashboard 不是黑盒

Dashboard 内联展示 SDK 片段,这一点对学习很重要。读者可以先用页面完成动作,再把对应 SDK 代码迁移到自己的脚本或应用里。推荐的学习方式是:

  • 先用页面执行一次成功交易。
  • 记录 digest、pool、BalanceManager、coin type 和数量。
  • 再用 SDK 构造同一类交易。
  • 最后用 Server 或链上查询确认结果。

常见误判

  • 页面没有立刻刷新,就以为交易失败。先看 digest 和 checkpoint,再看 indexer 是否追上。
  • Faucet 发币成功,但 Trading 页仍显示余额不足。检查钱包网络、coin type、BalanceManager 充值动作是否完成。
  • Market Maker 没有订单,直接怀疑撮合引擎。先检查 oracle 和 market maker health。
  • Deployment 页面地址变化,却还用旧配置调用 SDK。localnet 重置后必须更新配置。

本节验收

  • 能按 Health、Deployment、Faucet、Trading、Market Maker 的顺序完成一次检查。
  • 能解释 BalanceManager 创建按钮背后的 Move 对象变化。
  • 能把 dashboard 的一次交易迁移成 SDK 调用的输入参数。

ch16-05 部署产物和地址清单

返回本章

在 Sui Move 里,发布不是最后一步,发布之后的地址管理才是应用开发真正开始的地方。DeepBook Sandbox 会把本地部署过程中产生的 package ID、object ID、pool ID 和 oracle object 写入文件与服务,让后续合约、SDK、dashboard 和脚本能共享同一份事实。

关键产物

产物用途
sandbox/.env给容器和脚本读取 package ID、object ID、key、RPC、market maker 参数。
sandbox/deployments/localnet.json记录本次 localnet 部署的地址、池子、oracle 等 manifest。
sandbox/Pub.localnet.toml给 Sui CLI 和自定义 Move 合约解析本地已发布 package。
.external-packages/pnpm deploy-all 生成的本地依赖目录,自定义合约会引用它。

这些文件不是普通配置。它们连接了三类世界:Move 编译器看到的依赖、localnet 上真实发布的地址、应用运行时需要传入的对象 ID。

部署顺序的含义

Sandbox 的部署不是随便发布几个 package。它需要按依赖关系推进:

  1. 发布 token,得到 DEEP coin 类型和 treasury。
  2. 发布 core DeepBook,得到订单簿和 BalanceManager 相关能力。
  3. 发布 Pyth 和 USDC,为价格与稳定币测试准备对象。
  4. 发布 Margin 和 liquidation 相关 package。
  5. 创建 pool、oracle object、margin pool 和注册关系。
  6. 写入 manifest,启动 indexer/server/oracle/market maker。

这个顺序是 Move 开发中很好的教材。一个后发布的 package 如果依赖前一个 package,就不能只靠源码路径;在本地链上,它还要知道前一个 package 的发布地址。

manifest 怎么用

最直接的读取方式:

curl http://localhost:9009/manifest

拿到 manifest 后,不要急着全部复制到应用配置里。先区分:

  • package ID:交易构造和 Move call 需要。
  • shared object ID:Pool、Registry、BalanceManager、oracle object 等运行时对象需要。
  • coin type:泛型 type argument 需要。
  • checkpoint 起点:Indexer 或数据校验需要。

对 SDK 应用而言,最重要的是把 package ID、pool ID、coin type、RPC URL 放进同一份 network config。对自定义合约而言,最重要的是让 Move.tomlPub.localnet.toml 同步。

重置后的地址风险

执行 pnpm down 或重新部署后,localnet 状态会重建,很多地址会变化。读者应该养成一个习惯:每次开始实验,都先把当前 manifest 视为新的事实源。旧 digest 可以用来写学习笔记,但旧 object ID 不应继续进入新交易。

本节验收

  • 能说明 .envlocalnet.jsonPub.localnet.toml 的区别。
  • 能解释为什么本地发布后的地址会影响 Move 编译和 SDK 调用。
  • 能从 manifest 中挑出 SDK、合约和 indexer 分别需要的字段。

ch16-06 自定义 Move 合约

返回本章

Sandbox 对高级 Move 学习最有价值的部分,是它允许你在本地写一个真正依赖 DeepBook 的 Move package。你不是在模拟接口,也不是在伪造对象,而是在 localnet 上引用已经发布的 DeepBook package,并把自己的合约发布到同一条链。

从模板开始

官方仓库提供 example_contract 模板。典型流程是:

cd deepbook-sandbox
cp -r sandbox/packages/example_contract sandbox/packages/my_contract
cd sandbox/packages/my_contract

模板的意义不是减少几行文件创建,而是给你一个正确的依赖形状。自定义合约通常会依赖这些本地 package:

依赖典型用途
deepbookPool、BalanceManager、订单和核心交易能力。
tokenDEEP token 类型。
deepbook_marginMargin 相关对象和入口。
margin_liquidation清算逻辑。
pyth价格对象与 oracle 相关依赖。
usdc本地 USDC coin type。

Move.toml 的关键点

本地合约的 Move.toml 需要同时处理 dependency 和 environment:

[package]
name = "my_contract"
edition = "2024"

[dependencies]
deepbook = { local = "../../.external-packages/deepbook" }
token = { local = "../../.external-packages/token" }

[environments]
localnet = "<chain-id>"

<chain-id>sandbox/Pub.localnet.toml 读取。这个细节很重要:Move 编译器不是只看文件路径,它还要知道当前 build environment 对应哪条链。

发布到 localnet

sui client test-publish --build-env localnet --pubfile-path ../../Pub.localnet.toml

这里有两个参数需要认真理解:

  • --build-env localnet:选择 Move.toml 中的 localnet 环境。
  • --pubfile-path ../../Pub.localnet.toml:告诉 Sui CLI,DeepBook 等依赖已经在本地链上发布,地址记录在这个 pubfile 中。

没有 --pubfile-path,编译器可能能看到源码,却无法把依赖解析到当前 localnet 的发布地址。对依赖链上协议的 Move 合约来说,这是最常见也最隐蔽的错误之一。

如何设计自己的合约练习

第一版不要写复杂策略。建议先做三个小 wrapper:

  1. 读取或传入 Pool、BalanceManager、Clock 等对象,验证对象借用顺序。
  2. 调用一个只做校验或记录事件的函数,确认自定义 event 能被 indexer 读取。
  3. 再尝试组合 DeepBook 交易入口,观察 PTB 中对象和 coin 的流转。

当 wrapper 失败时,先看 abort module 和 abort code,再看对象类型和 type argument。Move 的错误往往不是“业务错了”,而是资源、权限、类型或对象版本没有对齐。

本节验收

  • 能复制 example_contract 并说明每个依赖的用途。
  • 能写出带 localnet environment 的 Move.toml
  • 能解释 --pubfile-path 为什么是依赖 DeepBook 的本地合约发布关键。

ch16-07 SDK、Indexer、Server 集成

返回本章

DeepBook 应用不能只靠一个 SDK,也不能只靠一个 REST API。SDK 负责构造交易,Indexer 负责把链上事件转成可查询读模型,Server 负责把读模型整理成应用可消费的接口。Sandbox 的好处是这三件事在本地同时存在,读者可以用一笔交易贯穿它们。

推荐调试闭环

一次交易的本地调试流程可以写成这样:

  1. 从 dashboard 或 manifest 获取 package ID、pool ID 和 coin type。
  2. 用 SDK 或 dashboard 构造交易,提交到 http://localhost:9000
  3. 保存 transaction digest。
  4. 用 Sui RPC 查询交易 effects 和 events。
  5. 等 indexer 追到对应 checkpoint。
  6. 用 DeepBook Server 查询订单、成交、池子或历史数据。
  7. 再回到 dashboard 看 UI 是否与读模型一致。

这套流程比“页面能用就行”严格得多,但它能帮你在真实应用里分清责任层。

SDK 在本地怎么配

本地 SDK 配置至少包含:

export const localnet = {
  rpcUrl: "http://localhost:9000",
  deepbookServerUrl: "http://localhost:9008",
  faucetUrl: "http://localhost:9009",
  manifestUrl: "http://localhost:9009/manifest",
};

实际项目里不要手写所有 ID。更好的方式是启动时读取 manifest,生成本地 network config,再把 config 注入交易服务。这样每次 pnpm down 后重新部署,都不会因为旧地址残留而构造错误交易。

Indexer 和 Server 怎么判断健康

命令行先看容器:

docker compose ps
docker compose logs -f deepbook-indexer
docker compose logs -f deepbook-server

再看 API 和 dashboard。需要注意的是,RPC 成功不等于 Server 已经能查到数据。Indexer 需要读取 checkpoint、解析事件、写 PostgreSQL,Server 才能返回最新读模型。交易刚提交后,页面短暂落后是正常现象;长期落后则要检查 indexer 日志和 checkpoint lag。

本地 API 的使用边界

Dashboard 会把 /api/deepbook 代理到 localhost:9008,但后端服务或脚本可以直接调用 http://localhost:9008。对于读者自己的应用,建议把链上查询和 Server 查询分开封装:

  • 链上查询用于最终状态、对象内容、交易 effects 和事件核验。
  • Server 查询用于订单列表、成交历史、池子数据、聚合展示和页面刷新。

这不是重复劳动,而是交易应用必须具备的双重证据。链上状态解释“发生了什么”,读模型解释“应用现在能展示什么”。

本节验收

  • 能用一笔交易串起 SDK、RPC、Indexer、Server 和 dashboard。
  • 能解释 RPC 成功但 UI 未刷新的原因。
  • 能设计一个读取 manifest 后生成本地 network config 的应用初始化流程。

ch16-08 Oracle、Market Maker 和 Faucet

返回本章

Sandbox 中最容易被低估的三个服务是 oracle、market maker 和 faucet。它们看起来像“辅助工具”,但没有它们,本地 DeepBook 很快会变成一个空壳:没有价格输入,没有订单簿深度,没有测试资产,读者只能看合约,无法真正交易。

Oracle Service

Oracle service 会维护本地 price object,让 SUI、DEEP、USDC 等资产在 localnet 上有可读价格。它的健康检查入口是:

curl http://localhost:9010/

对 Margin 和 market maker 来说,价格不是页面装饰,而是风控和报价的输入。Oracle 如果停止更新,market maker 可能停止合理报价,Margin 场景也无法正确演练风险。

Market Maker

Market maker 维护本地池子的双边报价。官方 README 描述的默认策略是网格型:围绕 oracle mid price 在买卖两边放置多个 POST_ONLY 限价单,并按固定间隔撤单、重算、重新挂单。

健康检查:

curl http://localhost:3001/health

查看日志:

docker compose logs -f market-maker

这对学习撮合引擎很有用。你可以先看 market maker 如何挂 maker-only 订单,再用自己的 taker 订单吃掉一部分深度,然后观察事件、余额和 Server 读模型。

Faucet

DeepBook faucet 与 Sui localnet 原生 faucet 不完全相同。原生 faucet 负责 SUI gas,DeepBook faucet 还能分发 DEEP 和 USDC。

请求 DEEP:

curl -X POST http://localhost:9009/faucet \
  -H "Content-Type: application/json" \
  -d '{"address":"0x<your-address>","token":"DEEP","amount":1000}'

请求 USDC:

curl -X POST http://localhost:9009/faucet \
  -H "Content-Type: application/json" \
  -d '{"address":"0x<your-address>","token":"USDC","amount":1000}'

请求 SUI:

curl -X POST http://localhost:9009/faucet \
  -H "Content-Type: application/json" \
  -d '{"address":"0x<your-address>","token":"SUI"}'

工程判断

这三个服务共同支撑“可交易性”:

  • Faucet 解决测试资金。
  • Oracle 解决价格输入。
  • Market maker 解决订单簿流动性。

如果本地交易失败,不要只看合约。先问自己:钱包有 gas 吗?BalanceManager 里有资产吗?池子有对手单吗?oracle 是否更新?market maker 是否健康?

本节验收

  • 能用 faucet 给测试地址发 SUI、DEEP 和 USDC。
  • 能检查 oracle 与 market maker 健康状态。
  • 能解释为什么本地订单簿需要自动做市服务。

ch16-09 测试、重置和数据管理

返回本章

Sandbox 的核心优点是可重置,核心风险也是可重置。它允许你每天从干净 localnet 开始,快速排除旧状态干扰;但如果你没有保存证据,重置会让 package ID、object ID、交易状态和数据库内容一起消失。

日常命令

cd deepbook-sandbox/sandbox

pnpm deploy-all
pnpm deploy-all --quick
docker compose ps
docker compose logs -f
docker compose logs -f market-maker
docker compose logs -f oracle-service
pnpm down

集成测试可用:

pnpm test:integration
pnpm test:integration <pattern>

重置前保存什么

每次准备 pnpm down 前,建议保存四类证据:

证据为什么要保存
deployments/localnet.json记录本轮 package、pool、oracle object 和部署关系。
交易 digest后续复盘交易 effects、events 和 abort 需要。
自定义合约源码localnet 会重置,源码不会;但发布地址会变。
关键日志indexer、server、oracle、market maker 的失败原因可能只在日志里。

pnpm down 会停止并清理容器、卷、链状态和自动生成的部分配置。它不会删除你的源码,但会让本地链重新开始。重新部署后,旧地址不能继续作为应用配置。

测试策略

把 sandbox 作为测试环境时,不要只写 happy path。至少覆盖:

  • Faucet 成功和余额不足。
  • BalanceManager 创建、重复发现、充值、提现。
  • 限价单、市价单、post-only 失败、撤单。
  • Oracle 不可用时 market maker 的行为。
  • Indexer 落后时前端如何显示 pending。
  • 重置后旧配置是否会被拒绝或自动刷新。

这些用例不一定都要写成自动化测试。对书稿和早期项目,runbook 加手工验收也有价值。关键是每一步能复现,有命令,有预期,有失败解释。

数据管理提醒

PostgreSQL 是读模型,不是资金事实源。重置数据库不会改变链上事实;重置 localnet 会让链上事实本身消失。开发者要明确自己当前是在清理读模型、清理容器,还是重建整条链。

这个区分在生产环境更重要。生产不能用“重置”解决数据问题,只能用重放、迁移、补偿任务、备份恢复和人工审计。

本节验收

  • 能说明 pnpm down 会清理什么、保留什么。
  • 能为一次本地交易保存 digest、manifest 和日志。
  • 能设计 sandbox 的最小回归测试清单。

ch16-10 用 Sandbox 承接本书练习

返回本章

前面章节的练习分散在 Move、SDK、Indexer、应用和部署里。Sandbox 可以把它们收束成一个本地学习闭环:读源码,跑本地链,执行交易,看事件,查读模型,改前端或脚本,再重置环境重来。

对应全书路线

本书章节Sandbox 中的练习方式
ch02 Move 基础用自定义合约模板练习 Move.toml、environment、发布和对象借用。
ch04 撮合源码观察 market maker 的 POST_ONLY 挂单,再手动吃单,复盘 fill。
ch05 BalanceManager用 dashboard 创建、充值、交易,理解账户余额与钱包余额的差异。
ch07 闪电贷在 localnet 中验证组合交易 wrapper 和失败路径。
ch09 Margin 应用利用本地 oracle、USDC、SUI pool 和 margin package 做风险演练。
ch12 SDK 集成从 dashboard SDK 片段迁移到自己的 TypeScript 服务。
ch13 Indexer/Server用交易 digest 对照 event、PostgreSQL 和 REST response。
ch14 应用构建把 dashboard 当参考产品,拆出自己的交易终端或机器人 MVP。
ch15 测试部署用 reset、logs、manifest、integration tests 形成交付证据。

推荐学习节奏

第一轮只跑通,不改代码。确认 dashboard、faucet、market maker 和交易页面都能工作。

第二轮开始记录证据。每做一个动作,记录输入对象、交易 digest、事件、读模型响应和 UI 变化。

第三轮改自定义合约。先写 wrapper,不急着做策略;先证明依赖、发布、对象传参和错误定位都清楚。

第四轮改应用。把 dashboard 里的流程拆成你自己的 SDK service、API client 和 UI 状态机。

第五轮做破坏性测试。停掉 oracle、让 indexer 落后、使用旧 manifest、让 faucet 地址错误,观察系统如何失败。

让练习不像文档

真正的练习不应该只是“运行命令得到输出”。可以把每个练习改成一个小问题:

  • 为什么创建 BalanceManager 要注册并共享对象?
  • 为什么 market maker 用 POST_ONLY,而不是普通限价单?
  • 为什么交易成功后 UI 仍可能短暂看不到成交?
  • 为什么自定义合约发布需要 Pub.localnet.toml
  • 为什么重置 localnet 后旧 SDK 配置会变成危险输入?

这些问题能把 DeepBook 学习从“会调用”推进到“懂系统”。

本节验收

  • 能把本书至少五章的练习迁移到 sandbox。
  • 能为每个练习写出链上证据、读模型证据和 UI 证据。
  • 能设计一个专属于自己项目的 sandbox 学习路线。

ch16-11 生产边界和迁移判断

返回本章

Sandbox 适合开发内环,不适合伪装成生产环境。它的价值是快、完整、可重置;生产环境的价值是稳定、可审计、可恢复、权限清晰。两者目标不同,不能混用。

哪些结论可以从 Sandbox 带走

可以带走:

  • PTB 构造是否合理。
  • BalanceManager、Pool、coin type、shared object 的传参是否正确。
  • 自定义 Move 合约能否依赖并调用 DeepBook。
  • 事件是否能被 indexer 读取并进入读模型。
  • 前端状态机是否能处理 pending、success、abort 和 indexer lag。
  • 做市、faucet、oracle、server 之间的交互是否符合预期。

不能直接带走:

  • 主网 package ID 和对象地址。
  • 生产 oracle 安全假设。
  • 生产 RPC 性能与稳定性结论。
  • 真实流动性和滑点结论。
  • 安全审计结论。
  • 生产部署拓扑和权限管理结论。

从 Sandbox 到测试网

迁移到测试网前,至少重做这些检查:

  1. 替换 network config、package ID、pool ID、coin type 和 server URL。
  2. 移除本地 faucet 假设,改用测试网资产获取流程。
  3. 检查钱包网络切换和签名提示。
  4. 检查真实 RPC 延迟、交易确认时间和失败重试。
  5. 检查 Indexer/Server 是否使用目标网络的数据源。
  6. 重新跑 dry run、失败路径和边界值测试。

从测试网到生产

生产环境要额外关注:

  • key 和权限:deployer、oracle、market maker、后端服务不能共用高权限 key。
  • 数据:PostgreSQL 需要备份、迁移、重放、只读账号和告警。
  • 服务:RPC、Indexer、Server、前端、交易服务要有健康检查和回滚策略。
  • 风控:机器人暂停条件、价格偏离阈值、最大仓位、最大下单量必须可配置。
  • 审计:自定义合约、后端交易构造、SDK 参数和权限边界都要有审查证据。

一个实用判断

如果某个问题在 sandbox 里都说不清,绝不要带到测试网;如果某个问题只能在测试网或生产暴露,就不要用 sandbox 的成功来证明它不存在。好的工程判断不是把一个环境用到底,而是知道每个环境能证明什么。

本节验收

  • 能列出 sandbox 可以证明和不能证明的事项。
  • 能写出从 sandbox 到测试网的迁移清单。
  • 能说明生产环境还需要哪些安全、数据和运维证据。

ch16-12 本章代码

返回本章

本章代码目录不是替代 DeepBook Sandbox 仓库,而是给读者一组随书运行手册。真实源码、脚本和服务以 MystenLabs/deepbook-sandbox 为准;本书代码目录负责说明怎么用、怎么验收、怎么把 sandbox 连接到前面章节。

目录

使用方式

建议读者先在独立目录克隆 sandbox,不要把官方仓库直接复制进本书 code/ 目录:

git clone --recurse-submodules https://github.com/MystenLabs/deepbook-sandbox.git
cd deepbook-sandbox/sandbox
pnpm install
cp .env.example .env
pnpm deploy-all

本章 code/ 下的 README 用来记录“怎么跑”和“怎么判断跑对了”。如果你在自己的项目里改了 SDK、合约或前端,可以把每次实验的 manifest、digest 和日志摘要写进这些目录的笔记中。

本章验收顺序

  1. 跑通 s01-sandbox-runbook
  2. s02-dashboard-checks 完成一次页面验收。
  3. s04-service-health-checks 检查 oracle、market maker、faucet 和 manifest。
  4. 最后进入 s03-custom-contract-template,发布自己的 Move package。

不要反过来。自定义合约依赖 .external-packages/Pub.localnet.toml,它们只有在 pnpm deploy-all 成功之后才可靠。