Skip to content

Commit c7eb33d

Browse files
committed
feat: init
1 parent 62f59b9 commit c7eb33d

7 files changed

Lines changed: 495 additions & 16 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Deploy to GitHub Pages
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- master
8+
9+
permissions:
10+
contents: read
11+
pages: write
12+
id-token: write
13+
14+
concurrency:
15+
group: "pages"
16+
cancel-in-progress: false
17+
18+
jobs:
19+
build:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
25+
- name: Setup mdBook
26+
uses: peaceiris/actions-mdbook@v1
27+
with:
28+
mdbook-version: "latest"
29+
30+
- name: Build
31+
run: mdbook build
32+
33+
- name: Setup Pages
34+
uses: actions/configure-pages@v4
35+
36+
- name: Upload artifact
37+
uses: actions/upload-pages-artifact@v3
38+
with:
39+
path: ./book
40+
41+
deploy:
42+
environment:
43+
name: github-pages
44+
url: ${{ steps.deployment.outputs.page_url }}
45+
runs-on: ubuntu-latest
46+
needs: build
47+
steps:
48+
- name: Deploy to GitHub Pages
49+
id: deployment
50+
uses: actions/deploy-pages@v4
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# 派生对象(Derived Object)
2+
3+
派生对象(Derived Object)是 Sui Framework 中用于**按父对象与键生成确定性地址**的机制。通过 `sui::derived_object`,你可以让某个对象的 ID 完全由「父对象 UID + 键」推导而出,从而实现可预测的地址、注册表去重以及按类型或键命名空间管理子对象。本节将详细介绍其 API、典型场景与注意事项。
4+
5+
## 为什么需要确定性地址
6+
7+
在默认情况下,`object::new(ctx)` 会为每个新对象分配一个**随机的**新 ID。但在以下场景中,我们更需要**确定性**的地址:
8+
9+
- **注册表(Registry)**:例如「每种代币类型 T 在 CoinRegistry 下对应唯一一个 Currency\<T\>」,希望同一类型 T 永远映射到同一个地址,便于链下按地址查询。
10+
- **命名空间**:父对象作为命名空间,不同键对应不同子对象地址,且同一键不能重复注册。
11+
- **可预测的 object ID**:前端或索引器希望在不发起交易的前提下,仅根据父 ID 和键就能算出子对象的 ID。
12+
13+
`derived_object` 提供的正是:**由 (父 UID, Key) 确定性地推导出 address/UID**,并在父对象上记录「该键已被占用」,从而保证同一键只能被 claim 一次。
14+
15+
## 与动态字段的关系
16+
17+
`derived_object` 在实现上依赖 **动态字段**`dynamic_field`):
18+
在父对象的 UID 上以 `Claimed(derived_id)` 为名存储一个标记,表示该派生 ID 已被占用。因此:
19+
20+
- **claim** 时会向父对象写入一条动态字段,用于防止同一 key 被重复 claim。
21+
- **exists** 时只是查询该动态字段是否存在,不创建新对象。
22+
- 派生出的 UID 一旦被 **claim**,就与父对象解耦使用,子对象可以独立存在、转移或共享,不要求父对象在交易中一起被访问(仅首次 claim 时需要父对象可变引用)。
23+
24+
## 模块与导入
25+
26+
```move
27+
use sui::derived_object;
28+
```
29+
30+
## 核心 API
31+
32+
| 函数 | 签名 | 说明 |
33+
|------|------|------|
34+
| **derive_address** | `fun derive_address<K: copy + drop + store>(parent: ID, key: K): address` | 根据父 ID 和键**计算**派生地址,不修改状态,不占用键。 |
35+
| **claim** | `fun claim<K: copy + drop + store>(parent: &mut UID, key: K): UID` | 在父对象上**占用**该键,返回对应的派生 UID;同一键重复 claim 会 abort。 |
36+
| **exists** | `fun exists<K: copy + drop + store>(parent: &UID, key: K): bool` | 查询该 (父, key) 是否已被 claim 过。 |
37+
38+
### derive_address
39+
40+
仅做**纯计算**:给定父对象的 `ID` 和键 `key`,返回一个确定的 `address`。不访问链上状态,不写入任何对象。可用于:
41+
42+
- 在未 claim 之前就预先知道「若用该 key claim,对象会落在哪个地址」。
43+
- 链下或前端用相同算法推算子对象 ID(需与框架实现保持一致)。
44+
45+
```move
46+
let parent_id = parent.id.to_inner();
47+
let addr = derived_object::derive_address(parent_id, my_key);
48+
// addr 每次对同一 parent_id + my_key 都相同
49+
```
50+
51+
### claim
52+
53+
**父对象的 UID** 上占用键 `key`,并返回一个**派生 UID**。内部会:
54+
55+
1.`derive_address(parent, key)` 得到地址并转成 ID;
56+
2. 检查父对象上是否已有 `Claimed(该 id)` 的动态字段;
57+
3. 若无,则添加该动态字段,并返回由该地址构造的 `UID`
58+
59+
返回的 UID 可直接用于构造新对象,使该对象「诞生」在派生地址上:
60+
61+
```move
62+
let derived_uid = derived_object::claim(&mut parent.id, key);
63+
let child = MyObject {
64+
id: derived_uid,
65+
field: value,
66+
};
67+
// child 的地址 = derive_address(parent.id.to_inner(), key)
68+
```
69+
70+
同一 `(parent, key)` 只能 **claim 一次**;再次 claim 会触发 **EObjectAlreadyExists** 并 abort。
71+
72+
### exists
73+
74+
查询在给定父对象上,某键是否已被 claim 过(即是否已存在对应的 `Claimed` 动态字段)。
75+
注意:一旦 claim 过,即使之后把派生出的对象删掉(`object::delete`),**exists 仍为 true**,该键无法再次 claim。这样设计是为了避免「删掉子对象后重新 claim 同一键得到新对象」,保证派生地址的长期唯一性。
76+
77+
## Key 的类型约束与唯一性
78+
79+
键类型 `K` 必须满足 **`copy + drop + store`**。常见用法:
80+
81+
- **简单类型**`u64``address``bool` 等。
82+
- **字符串**`std::string::String``std::ascii::String`(注意 `String``vector<u8>``ascii::String` 类型不同,会得到不同地址)。
83+
- **结构体**:如 `CurrencyKey<T>()` 这种单例式 key,用于「按类型 T 派生」。
84+
85+
不同**类型**或不同****的 key 会得到不同的派生地址。例如:
86+
87+
- `derive_address(parent, b"foo".to_string())``derive_address(parent, b"foo")``vector<u8>`**不等**
88+
- `derive_address(parent, key1)``derive_address(parent, key2)``key1 != key2`**不等**
89+
90+
因此设计注册表时,键的选取(类型 + 取值)要能唯一标识一个「槽位」。
91+
92+
## 典型场景
93+
94+
### 1. 按类型注册:每个 T 一个槽位
95+
96+
在类型注册表(如 CoinRegistry)中,希望「每种类型 T 对应一个对象」。可以用**类型相关的 key**(例如一个只包含类型的结构体)作为键:
97+
98+
```move
99+
use sui::derived_object;
100+
101+
public struct Registry has key { id: UID }
102+
103+
/// 用作派生键:同一类型 T 总是同一个 Key
104+
public struct TypeKey<phantom T> has copy, drop, store {}
105+
106+
public fun register<T: key>(
107+
registry: &mut Registry,
108+
ctx: &mut TxContext,
109+
): UID {
110+
derived_object::claim(&mut registry.id, TypeKey<T>())
111+
}
112+
113+
public fun exists<T: key>(registry: &Registry): bool {
114+
derived_object::exists(&registry.id, TypeKey<T>())
115+
}
116+
```
117+
118+
这样每种 `T` 最多被注册一次,且对应地址唯一、可复现。
119+
120+
### 2. 按字符串键注册:命名槽位
121+
122+
用字符串(或其它业务键)做命名空间,每个键对应一个派生对象:
123+
124+
```move
125+
public fun create_named_slot(
126+
registry: &mut Registry,
127+
name: std::string::String,
128+
ctx: &mut TxContext,
129+
): UID {
130+
derived_object::claim(&mut registry.id, name)
131+
}
132+
133+
public fun slot_exists(registry: &Registry, name: std::string::String): bool {
134+
derived_object::exists(&registry.id, name)
135+
}
136+
```
137+
138+
### 3. 先算地址再创建对象
139+
140+
若希望「先知道地址,再在后续逻辑里创建对象」,可以先用 `derive_address` 得到地址,再在需要时 `claim` 并用返回的 UID 构造对象:
141+
142+
```move
143+
// 仅计算,不占用
144+
let addr = derived_object::derive_address(registry.id.to_inner(), my_key);
145+
146+
// 需要时再占用并创建对象
147+
let uid = derived_object::claim(&mut registry.id, my_key);
148+
let obj = MyRecord { id: uid, data: ... };
149+
```
150+
151+
## 完整示例:简单类型注册表
152+
153+
下面示例实现一个「按类型 T 注册单例对象」的注册表,并用派生对象保证每种类型只有一个实例、地址确定:
154+
155+
```move
156+
module examples::type_registry;
157+
158+
use sui::derived_object;
159+
use sui::transfer;
160+
use std::string::String;
161+
162+
public struct Registry has key {
163+
id: UID,
164+
}
165+
166+
/// 每种类型 T 对应一个「槽位」键
167+
public struct TypeKey<phantom T> has copy, drop, store {}
168+
169+
/// 注册表中每类 T 存一条记录
170+
public struct Record<T: store> has key {
171+
id: UID,
172+
name: String,
173+
value: T,
174+
}
175+
176+
public fun new_registry(ctx: &mut TxContext): Registry {
177+
Registry { id: object::new(ctx) }
178+
}
179+
180+
/// 为类型 T 注册一条记录;若 T 已注册则 abort
181+
public fun register<T: key + store>(
182+
registry: &mut Registry,
183+
name: String,
184+
value: T,
185+
ctx: &mut TxContext,
186+
) {
187+
assert!(!derived_object::exists(&registry.id, TypeKey<T>()), 0);
188+
let uid = derived_object::claim(&mut registry.id, TypeKey<T>());
189+
let record = Record<T> { id: uid, name, value };
190+
transfer::share_object(record); // 或 transfer::transfer(record, ctx.sender())
191+
}
192+
193+
public fun is_registered<T: key>(registry: &Registry): bool {
194+
derived_object::exists(&registry.id, TypeKey<T>())
195+
}
196+
```
197+
198+
要点:
199+
200+
-**TypeKey\<T\>** 做键,保证「每种 T 一个槽位」。
201+
- **register** 中先 `exists``claim`,避免重复注册。
202+
- **claim** 得到的 UID 直接用作 `Record``id`,这样 `Record<T>` 的 object ID 永远由 `(Registry.id, TypeKey<T>)` 确定。
203+
204+
## 在 CoinRegistry 中的用法
205+
206+
Sui 的 **CoinRegistry****finalize_registration** 中使用了派生对象:
207+
当一种新代币的 `Currency<T>` 被「注册」到链上时,会从 CoinRegistry 的 UID 和 **CurrencyKey\<T\>** 派生出该 Currency 的 UID,并作为共享对象发布。这样:
208+
209+
- 每种代币类型 `T` 在全局只有一个 `Currency<T>` 对象;
210+
- 其地址由 `(CoinRegistry.id, CurrencyKey<T>)` 确定,索引器和前端可以稳定地按类型推算或查询。
211+
212+
你不需要自己实现该逻辑,但理解「派生对象 = 父 + 键 → 确定性 UID」有助于阅读框架中各类 Registry 的实现。
213+
214+
## 注意事项
215+
216+
1. **claim 不可逆**:一旦对某 (parent, key) 调用了 **claim**,该键就永远被视为已占用;即使之后用返回的 UID 创建的对象被删掉,**exists** 仍为 true,不能再次 claim 同一 key。
217+
2. **键的类型与值都要一致**:链下或前端若想复现地址,键的类型和值必须与链上完全一致(例如都用 `String` 且内容相同)。
218+
3. **父对象需可变**:只有 **claim** 需要 `&mut UID`**derive_address****exists** 只需 `&UID``ID`
219+
4. **派生出的对象独立存在**:claim 返回的 UID 用于构造对象后,该对象与普通对象一样可以 transfer、share、freeze,不要求父对象同时存在或可访问(仅首次 claim 时需要父对象)。
220+
221+
## 小结
222+
223+
- **derived_object** 提供由 **(父 UID, Key)** 确定性地推导 **address/UID** 的能力,并保证同一键只能被 **claim** 一次。
224+
- **derive_address** 只做计算;**claim** 占用键并返回 UID,用于在派生地址上创建对象;**exists** 查询键是否已被占用。
225+
- 常用于**注册表****按类型或名称的命名空间**,以及需要**可预测 object ID** 的场景。
226+
- 实现上依赖动态字段在父对象上记录「已占用的派生 ID」;派生出的对象之后可独立于父对象使用。

src/08_programmability/index.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@
1414
| 8.6 | 集合类型 | VecMap / VecSet 的使用 |
1515
| 8.7 | 动态字段 | 异构存储、增删改查 |
1616
| 8.8 | 动态对象字段 | 与动态字段的区别、链上可查询 |
17-
| 8.9 | 动态集合 | Table / Bag / ObjectTable / ObjectBag |
18-
| 8.10 | Balance 与 Coin | 代币的底层操作 |
19-
| 8.11 | BCS 序列化 | 编码 / 解码、链下参数构造 |
20-
| 8.12 | 密码学与哈希 | SHA / ED25519 / ECDSA |
21-
| 8.13 | 链上随机数 | Random 对象、公平性保证 |
17+
| 8.9 | 派生对象(derived_object) | 确定性地址、claim/exists、注册表模式 |
18+
| 8.10 | 动态集合 | Table / Bag / ObjectTable / ObjectBag |
19+
| 8.11 | Balance 与 Coin | 代币的底层操作 |
20+
| 8.12 | BCS 序列化 | 编码 / 解码、链下参数构造 |
21+
| 8.13 | 密码学与哈希 | SHA / ED25519 / ECDSA |
22+
| 8.14 | 链上随机数 | Random 对象、公平性保证 |
2223

2324
## 学习目标
2425

2526
读完本章后,你将能够:
2627

27-
- 使用动态字段实现灵活的数据存储
28+
- 使用动态字段与动态对象字段实现灵活的数据存储
29+
- 使用派生对象(derived_object)实现确定性地址与注册表模式
2830
- 操作 Balance 和 Coin 实现代币逻辑
2931
- 在合约中使用密码学原语和链上随机数

0 commit comments

Comments
 (0)