@0xobelisk/sui-common 1.2.0-pre.12 → 1.2.0-pre.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +670 -1
- package/dist/index.d.ts +77 -45
- package/dist/index.js +1205 -204
- package/dist/index.js.map +1 -1
- package/package.json +20 -22
- package/src/codegen/debug.ts +0 -4
- package/src/codegen/types/index.ts +60 -73
- package/src/codegen/utils/config.ts +1 -1
- package/src/codegen/utils/format.ts +0 -6
- package/src/codegen/utils/formatAndWrite.ts +11 -32
- package/src/codegen/utils/generateLock.ts +122 -0
- package/src/codegen/utils/index.ts +5 -2
- package/src/codegen/utils/renderMove/codegen.ts +103 -0
- package/src/codegen/utils/renderMove/common.ts +0 -64
- package/src/codegen/utils/renderMove/dapp.ts +7 -0
- package/src/codegen/utils/renderMove/generateDappKey.ts +26 -12
- package/src/codegen/utils/renderMove/generateEnums.ts +57 -0
- package/src/codegen/utils/renderMove/generateError.ts +30 -26
- package/src/codegen/utils/renderMove/generateGenesis.ts +64 -0
- package/src/codegen/utils/renderMove/generateInitTest.ts +37 -0
- package/src/codegen/utils/renderMove/generateObjects.ts +373 -0
- package/src/codegen/utils/renderMove/generatePermits.ts +148 -0
- package/src/codegen/utils/renderMove/generateResources.ts +1471 -0
- package/src/codegen/utils/renderMove/generateScenes.ts +454 -0
- package/src/codegen/utils/renderMove/generateScript.ts +19 -15
- package/src/codegen/utils/renderMove/generateSystem.ts +0 -2
- package/src/codegen/utils/renderMove/generateToml.ts +2 -6
- package/src/codegen/utils/renderMove/generateUserStorageInit.ts +33 -0
- package/src/codegen/utils/validateConfig.ts +222 -0
- package/src/index.ts +0 -1
- package/src/modules.d.ts +0 -10
- package/src/codegen/modules.d.ts +0 -1
- package/src/codegen/utils/posixPath.ts +0 -8
- package/src/codegen/utils/renderMove/generateDefaultSchema.ts +0 -216
- package/src/codegen/utils/renderMove/generateEvent.ts +0 -98
- package/src/codegen/utils/renderMove/generateInit.ts +0 -79
- package/src/codegen/utils/renderMove/generateSchema.ts +0 -274
- package/src/codegen/utils/renderMove/generateSchemaHub.ts +0 -62
- package/src/codegen/utils/renderMove/schemaGen.ts +0 -65
- package/src/parseData/index.ts +0 -1
- package/src/parseData/parser/index.ts +0 -47
package/README.md
CHANGED
|
@@ -1,3 +1,672 @@
|
|
|
1
1
|
# @0xobelisk/sui-common
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Dubhe 框架的底层公共逻辑包。提供 `generate` 代码生成器(供 CLI 调用)、`DubheConfig` 类型定义,以及 CLI 和 SDK 共用的工具函数。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 包导出结构
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
@0xobelisk/sui-common
|
|
11
|
+
├── codegen/ # generate —— 根据 DubheConfig 生成 Move 源文件
|
|
12
|
+
│ ├── types/ # DubheConfig、MoveType、Component 类型定义
|
|
13
|
+
│ └── utils/ # schemaGen() 入口 + 所有生成器模块
|
|
14
|
+
├── parseData/ # 运行时工具:解析链上 BCS 响应数据
|
|
15
|
+
└── primitives/ # SDK 使用的订阅类型枚举
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## DubheConfig 完整参考
|
|
21
|
+
|
|
22
|
+
`DubheConfig` 是所有 Move 源文件的唯一配置来源。
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { defineConfig } from '@0xobelisk/sui-common';
|
|
26
|
+
|
|
27
|
+
export const dubheConfig = defineConfig({
|
|
28
|
+
name: 'my_project', // Move 包名(snake_case)
|
|
29
|
+
description: 'My description',
|
|
30
|
+
enums: { ... }, // 可选:自定义枚举类型
|
|
31
|
+
resources: { ... }, // 必填:数据 Schema 定义
|
|
32
|
+
errors: { ... }, // 可选:自定义错误消息
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### `name` / `description`
|
|
37
|
+
|
|
38
|
+
`name` 会成为 Move 包名和模块前缀,必须是合法的 Move 标识符(snake_case,不能含连字符)。
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
### `enums`
|
|
43
|
+
|
|
44
|
+
定义可在 resource 字段类型中使用的自定义枚举类型。
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
enums: {
|
|
48
|
+
Direction: ['North', 'East', 'South', 'West'],
|
|
49
|
+
Status: ['Missed', 'Caught', 'Fled'],
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
每个枚举会在 `sources/codegen/enums/<枚举名>.move` 生成一个 Move 模块,包含:
|
|
54
|
+
|
|
55
|
+
| 生成内容 | 说明 |
|
|
56
|
+
| ------------------------------------- | --------------------------------------- |
|
|
57
|
+
| `public enum <Name>` | 带 `copy, drop, store` 能力的 Move 枚举 |
|
|
58
|
+
| `public fun new_<variant>()` | 每个变体的构造函数 |
|
|
59
|
+
| `public fun encode(self): vector<u8>` | BCS 序列化 |
|
|
60
|
+
| `public fun decode(bytes: &mut BCS)` | BCS 反序列化 |
|
|
61
|
+
|
|
62
|
+
**生成示例:**
|
|
63
|
+
|
|
64
|
+
```move
|
|
65
|
+
module my_project::direction {
|
|
66
|
+
public enum Direction has copy, drop, store { East, North, South, West }
|
|
67
|
+
public fun new_east(): Direction { Direction::East }
|
|
68
|
+
public fun encode(self: Direction): vector<u8> { to_bytes(&self) }
|
|
69
|
+
public fun decode(bytes: &mut BCS): Direction { ... }
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### `resources`
|
|
76
|
+
|
|
77
|
+
核心数据 Schema。`resources` 中的每个条目对应一个 Move 模块,生成到 `sources/codegen/resources/<名称>.move`。
|
|
78
|
+
|
|
79
|
+
#### 六种 Resource 模式
|
|
80
|
+
|
|
81
|
+
根据 `fields`、`keys`、`global`、`offchain` 的组合,共有 **六种** 模式。
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
##### 模式 1 —— 简写单值(entity 级存储)
|
|
86
|
+
|
|
87
|
+
最简单的形式:直接写一个原始类型或枚举类型字符串。值按 `resource_account`(实体地址)存储。
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
resources: {
|
|
91
|
+
health: 'u32',
|
|
92
|
+
status: 'Status', // 枚举类型
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
生成的 API:
|
|
97
|
+
|
|
98
|
+
```move
|
|
99
|
+
public fun has(dapp_hub: &DappHub, resource_account: String): bool
|
|
100
|
+
public fun ensure_has(dapp_hub: &DappHub, resource_account: String)
|
|
101
|
+
public fun ensure_has_not(dapp_hub: &DappHub, resource_account: String)
|
|
102
|
+
public(package) fun delete(dapp_hub: &mut DappHub, resource_account: String)
|
|
103
|
+
public fun get(dapp_hub: &DappHub, resource_account: String): u32
|
|
104
|
+
public(package) fun set(dapp_hub: &mut DappHub, resource_account: String, value: u32, ctx: &mut TxContext)
|
|
105
|
+
public fun encode(value: u32): vector<vector<u8>>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
##### 模式 2 —— 多字段 entity 资源(无显式 keys)
|
|
111
|
+
|
|
112
|
+
`fields` 中没有设置 `keys`,只用 `resource_account` 作为存储键。这是 "component" 风格——每个实体(地址)拥有一条记录。
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
resources: {
|
|
116
|
+
stats: {
|
|
117
|
+
fields: { attack: 'u32', hp: 'u32' }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
生成的 API:
|
|
123
|
+
|
|
124
|
+
```move
|
|
125
|
+
public fun has(dapp_hub: &DappHub, resource_account: String): bool
|
|
126
|
+
public fun ensure_has(dapp_hub: &DappHub, resource_account: String)
|
|
127
|
+
public fun ensure_has_not(dapp_hub: &DappHub, resource_account: String)
|
|
128
|
+
public(package) fun delete(dapp_hub: &mut DappHub, resource_account: String)
|
|
129
|
+
public fun get(dapp_hub: &DappHub, resource_account: String): (u32, u32)
|
|
130
|
+
public(package) fun set(dapp_hub: &mut DappHub, resource_account: String, attack: u32, hp: u32, ctx: &mut TxContext)
|
|
131
|
+
// 每个字段单独的 getter / setter:
|
|
132
|
+
public fun get_attack(dapp_hub: &DappHub, resource_account: String): u32
|
|
133
|
+
public(package) fun set_attack(dapp_hub: &mut DappHub, resource_account: String, attack: u32, ctx: &mut TxContext)
|
|
134
|
+
public fun get_hp(...): u32
|
|
135
|
+
public(package) fun set_hp(...)
|
|
136
|
+
// 结构体级别的 getter / setter:
|
|
137
|
+
public fun get_struct(dapp_hub: &DappHub, resource_account: String): Stats
|
|
138
|
+
public(package) fun set_struct(dapp_hub: &mut DappHub, resource_account: String, stats: Stats, ctx: &mut TxContext)
|
|
139
|
+
// 编解码:
|
|
140
|
+
public fun encode(attack: u32, hp: u32): vector<vector<u8>>
|
|
141
|
+
public fun encode_struct(stats: Stats): vector<vector<u8>>
|
|
142
|
+
public fun decode(data: vector<u8>): Stats
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
同时会生成 `Stats` 结构体(具有 `copy, drop, store` 能力),包含字段读写方法:
|
|
146
|
+
`public fun attack(self: &Stats): u32` 和 `public fun update_attack(self: &mut Stats, attack: u32)`。
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
##### 模式 3 —— 单值 keyed 资源
|
|
151
|
+
|
|
152
|
+
一个值字段 + 显式 `keys`,存储键为 `[TABLE_NAME, ...key_bytes]`。
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
resources: {
|
|
156
|
+
player_score: {
|
|
157
|
+
fields: { player: 'address', score: 'u32' },
|
|
158
|
+
keys: ['player']
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
生成的 API(key 字段作为额外参数,排在 `resource_account` 之后):
|
|
164
|
+
|
|
165
|
+
```move
|
|
166
|
+
public fun has(dapp_hub: &DappHub, resource_account: String, player: address): bool
|
|
167
|
+
public fun ensure_has(dapp_hub: &DappHub, resource_account: String, player: address)
|
|
168
|
+
public fun ensure_has_not(dapp_hub: &DappHub, resource_account: String, player: address)
|
|
169
|
+
public(package) fun delete(dapp_hub: &mut DappHub, resource_account: String, player: address)
|
|
170
|
+
public fun get(dapp_hub: &DappHub, resource_account: String, player: address): u32
|
|
171
|
+
public(package) fun set(dapp_hub: &mut DappHub, resource_account: String, player: address, score: u32, ctx: &mut TxContext)
|
|
172
|
+
public fun encode(value: u32): vector<vector<u8>>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
##### 模式 4 —— 多字段 keyed 资源
|
|
178
|
+
|
|
179
|
+
多个值字段 + 显式 `keys`,除元组级的 `get`/`set` 外,还会生成各字段独立的 getter/setter。
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
resources: {
|
|
183
|
+
player_stats: {
|
|
184
|
+
fields: { player: 'address', attack: 'u32', hp: 'u32' },
|
|
185
|
+
keys: ['player']
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
生成的 API:
|
|
191
|
+
|
|
192
|
+
```move
|
|
193
|
+
public fun has(..., player: address): bool
|
|
194
|
+
public fun ensure_has(..., player: address)
|
|
195
|
+
public fun ensure_has_not(..., player: address)
|
|
196
|
+
public(package) fun delete(..., player: address)
|
|
197
|
+
public fun get(..., player: address): (u32, u32)
|
|
198
|
+
public(package) fun set(..., player: address, attack: u32, hp: u32, ctx: &mut TxContext)
|
|
199
|
+
public fun get_attack(..., player: address): u32
|
|
200
|
+
public(package) fun set_attack(..., player: address, attack: u32, ctx: &mut TxContext)
|
|
201
|
+
// get_hp / set_hp 类似...
|
|
202
|
+
public fun get_struct(..., player: address): PlayerStats
|
|
203
|
+
public(package) fun set_struct(..., player: address, player_stats: PlayerStats, ctx: &mut TxContext)
|
|
204
|
+
public fun encode(...): vector<vector<u8>>
|
|
205
|
+
public fun encode_struct(...): vector<vector<u8>>
|
|
206
|
+
public fun decode(data: vector<u8>): PlayerStats
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
##### 模式 5 —— Global 单例资源
|
|
212
|
+
|
|
213
|
+
设置 `global: true` 后,以 `dapp_key::package_id()` 作为固定的 `resource_account`,API 中不再暴露 `resource_account` 参数。
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
resources: {
|
|
217
|
+
game_config: {
|
|
218
|
+
global: true,
|
|
219
|
+
fields: { max_players: 'u32', fee: 'u64' }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
生成的 API(无 `resource_account` 参数):
|
|
225
|
+
|
|
226
|
+
```move
|
|
227
|
+
public fun has(dapp_hub: &DappHub): bool
|
|
228
|
+
public fun ensure_has(dapp_hub: &DappHub)
|
|
229
|
+
public fun ensure_has_not(dapp_hub: &DappHub)
|
|
230
|
+
public(package) fun delete(dapp_hub: &mut DappHub)
|
|
231
|
+
public fun get(dapp_hub: &DappHub): (u32, u64)
|
|
232
|
+
public(package) fun set(dapp_hub: &mut DappHub, max_players: u32, fee: u64, ctx: &mut TxContext)
|
|
233
|
+
// get_max_players / set_max_players / get_fee / set_fee ...
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`global: true` 与显式 `keys` 可以同时使用,keys 仍然作为参数出现。
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
##### 模式 6 —— Offchain 资源
|
|
241
|
+
|
|
242
|
+
设置 `offchain: true` 后,所有读取函数(`get`、`get_*`、`get_struct`、`has`、`ensure_has`、`ensure_has_not`)都不会生成,只保留 `set` / `set_struct` / `encode` / `delete`。适用于数据由链外索引器读取的场景。
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
resources: {
|
|
246
|
+
event_log: {
|
|
247
|
+
offchain: true,
|
|
248
|
+
fields: { timestamp: 'u64', message: 'String' }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
#### `Component` 全部选项说明
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
type Component = {
|
|
259
|
+
fields: Record<string, MoveType>; // 必填:字段名 -> Move 类型
|
|
260
|
+
keys?: string[]; // 可选:哪些字段参与存储键
|
|
261
|
+
global?: boolean; // 可选:使用 package address 作为 resource_account
|
|
262
|
+
offchain?: boolean; // 可选:不生成读取函数
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### `errors`
|
|
269
|
+
|
|
270
|
+
自定义命名错误及其人类可读的消息。
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
errors: {
|
|
274
|
+
NotAuthorized: 'Caller is not authorized',
|
|
275
|
+
ResourceNotFound: 'The requested resource does not exist',
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
生成 `sources/codegen/errors.move`:
|
|
280
|
+
|
|
281
|
+
```move
|
|
282
|
+
module my_project::errors {
|
|
283
|
+
#[error]
|
|
284
|
+
const NOTAUTHORIZED: vector<u8> = b"Caller is not authorized";
|
|
285
|
+
public fun not_authorized_error(condition: bool) { assert!(condition, NOTAUTHORIZED) }
|
|
286
|
+
|
|
287
|
+
#[error]
|
|
288
|
+
const RESOURCENOTFOUND: vector<u8> = b"The requested resource does not exist";
|
|
289
|
+
public fun resource_not_found_error(condition: bool) { assert!(condition, RESOURCENOTFOUND) }
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 支持的 Move 类型
|
|
296
|
+
|
|
297
|
+
| TypeScript 字符串 | Move 类型 |
|
|
298
|
+
| ---------------------- | ----------------------------------- |
|
|
299
|
+
| `'address'` | `address` |
|
|
300
|
+
| `'bool'` | `bool` |
|
|
301
|
+
| `'u8'` | `u8` |
|
|
302
|
+
| `'u32'` | `u32` |
|
|
303
|
+
| `'u64'` | `u64` |
|
|
304
|
+
| `'u128'` | `u128` |
|
|
305
|
+
| `'u256'` | `u256` |
|
|
306
|
+
| `'String'` | `std::ascii::String` |
|
|
307
|
+
| `'vector<u8>'` | `vector<u8>` |
|
|
308
|
+
| `'vector<u32>'` | `vector<u32>` |
|
|
309
|
+
| `'vector<u64>'` | `vector<u64>` |
|
|
310
|
+
| `'vector<u128>'` | `vector<u128>` |
|
|
311
|
+
| `'vector<u256>'` | `vector<u256>` |
|
|
312
|
+
| `'vector<address>'` | `vector<address>` |
|
|
313
|
+
| `'vector<bool>'` | `vector<bool>` |
|
|
314
|
+
| `'vector<vector<u8>>'` | `vector<vector<u8>>` |
|
|
315
|
+
| `'<EnumName>'` | 自定义枚举(必须在 `enums` 中声明) |
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## generate 执行流程
|
|
320
|
+
|
|
321
|
+
`schemaGen(rootDir, config, network?)` 是 `dubhe generate` 命令调用的主入口,**按顺序**执行以下步骤:
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
generate
|
|
325
|
+
│
|
|
326
|
+
├── 1. 删除 sources/codegen/ (每次都清空重生成)
|
|
327
|
+
│
|
|
328
|
+
├── 2. Move.toml (仅首次生成,不覆盖)
|
|
329
|
+
│ └─ 锁定 Sui 和 Dubhe 的依赖版本
|
|
330
|
+
│
|
|
331
|
+
├── 3. sources/codegen/genesis.move (仅首次生成,不覆盖)
|
|
332
|
+
│ └─ entry fun run():注册 dapp + 调用 deploy_hook
|
|
333
|
+
│
|
|
334
|
+
├── 4. sources/codegen/init_test.move (仅首次生成,不覆盖)
|
|
335
|
+
│ └─ deploy_dapp_for_testing(),用于 Move 单元测试
|
|
336
|
+
│
|
|
337
|
+
├── 5. sources/codegen/dapp_key.move (仅首次生成,不覆盖)
|
|
338
|
+
│ └─ DappKey 结构体 + to_string() + package_id() + eq()
|
|
339
|
+
│
|
|
340
|
+
├── 6. sources/scripts/deploy_hook.move (仅首次生成,不覆盖)
|
|
341
|
+
│ └─ 可编辑的钩子,由 genesis 调用,用于添加部署后初始化逻辑
|
|
342
|
+
│
|
|
343
|
+
├── 7. sources/codegen/resources/ (每次都重新生成)
|
|
344
|
+
│ └─ DubheConfig 中每个 resource 条目生成一个 .move 文件
|
|
345
|
+
│
|
|
346
|
+
├── 8. sources/codegen/enums/ (仅首次生成,不覆盖)
|
|
347
|
+
│ └─ DubheConfig 中每个 enum 条目生成一个 .move 文件
|
|
348
|
+
│
|
|
349
|
+
├── 9. sources/codegen/errors.move (如果 config.errors 存在则生成)
|
|
350
|
+
│
|
|
351
|
+
├── 10. sources/systems/ (目录不存在时创建,已存在不修改)
|
|
352
|
+
│ └─ 手写 system 业务逻辑的占位目录
|
|
353
|
+
│
|
|
354
|
+
├── 11. sources/tests/ (目录不存在时创建,已存在不修改)
|
|
355
|
+
│ └─ 手写 Move 测试的占位目录
|
|
356
|
+
│
|
|
357
|
+
└── 12. sources/scripts/migrate.move (仅首次生成,不覆盖)
|
|
358
|
+
└─ ON_CHAIN_VERSION 常量,用于合约升级版本管理
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**关键重生成规则:** 只有 `sources/codegen/resources/`(第 7 步)每次都会重新生成。其他所有文件只在不存在时才生成——**用户可编辑的文件永远不会被覆盖**。
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 生成文件目录结构
|
|
366
|
+
|
|
367
|
+
对名为 `my_project` 的项目执行 `dubhe generate` 后,目录结构如下:
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
src/my_project/
|
|
371
|
+
├── Move.toml
|
|
372
|
+
└── sources/
|
|
373
|
+
├── codegen/
|
|
374
|
+
│ ├── genesis.move # 自动生成,禁止手动编辑
|
|
375
|
+
│ ├── init_test.move # 自动生成,禁止手动编辑
|
|
376
|
+
│ ├── dapp_key.move # 自动生成,禁止手动编辑
|
|
377
|
+
│ ├── errors.move # 自动生成,禁止手动编辑
|
|
378
|
+
│ ├── resources/
|
|
379
|
+
│ │ ├── health.move # 自动生成,禁止手动编辑
|
|
380
|
+
│ │ └── stats.move # 自动生成,禁止手动编辑
|
|
381
|
+
│ └── enums/
|
|
382
|
+
│ └── direction.move # 自动生成,禁止手动编辑
|
|
383
|
+
├── scripts/
|
|
384
|
+
│ ├── deploy_hook.move # 可编辑:部署后初始化逻辑
|
|
385
|
+
│ └── migrate.move # 可编辑:升级版本跟踪
|
|
386
|
+
├── systems/ # 可编辑:游戏 / 应用业务逻辑
|
|
387
|
+
└── tests/ # 可编辑:Move 单元测试
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Framework 架构解析
|
|
395
|
+
|
|
396
|
+
### 什么是 Dubhe Framework?
|
|
397
|
+
|
|
398
|
+
`generate` 生成的 Move 合约代码并非独立运行,它必须搭配 **Dubhe Framework**(链上已部署的 `dubhe` 包)才能工作。Framework 是一个已部署在 Sui 链上的基础设施包,为所有通过 Dubhe 构建的 DApp 提供存储引擎、事件系统、费用计量、权限管理和版本控制能力。
|
|
399
|
+
|
|
400
|
+
可以这样理解两者的关系:
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
你写的 DubheConfig(TypeScript)
|
|
404
|
+
│
|
|
405
|
+
▼ dubhe generate
|
|
406
|
+
你的合约代码(Move)── 调用 ──▶ Dubhe Framework(链上已部署)
|
|
407
|
+
│
|
|
408
|
+
▼
|
|
409
|
+
DappHub(链上共享对象)
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### 整体架构分层
|
|
415
|
+
|
|
416
|
+
```
|
|
417
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
418
|
+
│ 你的 DApp 合约包 │
|
|
419
|
+
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
|
|
420
|
+
│ │ sources/systems/ │ │ sources/scripts/ │ │
|
|
421
|
+
│ │ 手写业务逻辑(可编辑) │ │ deploy_hook / migrate │ │
|
|
422
|
+
│ └────────────┬─────────────┘ └──────────────────────────┘ │
|
|
423
|
+
│ │ 调用 │
|
|
424
|
+
│ ┌────────────▼─────────────────────────────────────────┐ │
|
|
425
|
+
│ │ sources/codegen/resources/(自动生成,禁止手动编辑) │ │
|
|
426
|
+
│ │ health.move / stats.move / player_score.move … │ │
|
|
427
|
+
│ └────────────┬─────────────────────────────────────────┘ │
|
|
428
|
+
└───────────────┼─────────────────────────────────────────────┘
|
|
429
|
+
│ 调用 dubhe::dapp_system / dubhe::dapp_service
|
|
430
|
+
▼
|
|
431
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
432
|
+
│ Dubhe Framework(链上已部署的 dubhe 包) │
|
|
433
|
+
│ │
|
|
434
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
435
|
+
│ │ 系统层 dubhe::dapp_system │ │
|
|
436
|
+
│ │ · set_record / set_field / delete_record │ │
|
|
437
|
+
│ │ · get_record / get_field / has_record │ │
|
|
438
|
+
│ │ · create_dapp / upgrade_dapp │ │
|
|
439
|
+
│ │ · charge_fee(写入费用计量) │ │
|
|
440
|
+
│ └──────────────────────┬──────────────────────────────┘ │
|
|
441
|
+
│ │ │
|
|
442
|
+
│ ┌──────────────────────▼──────────────────────────────┐ │
|
|
443
|
+
│ │ 核心层 dubhe::dapp_service │ │
|
|
444
|
+
│ │ · DappHub(全局共享对象,链上统一存储中心) │ │
|
|
445
|
+
│ │ · ObjectTable<AccountKey, AccountData> │ │
|
|
446
|
+
│ │ · dynamic_field 读写(实际数据存储) │ │
|
|
447
|
+
│ │ · 发射 SetRecord / DeleteRecord 事件 │ │
|
|
448
|
+
│ └──────────────────────┬──────────────────────────────┘ │
|
|
449
|
+
│ │ │
|
|
450
|
+
│ ┌──────────────────────▼──────────────────────────────┐ │
|
|
451
|
+
│ │ 内置资源 dubhe::dapp_metadata / dapp_fee_state │ │
|
|
452
|
+
│ │ / dapp_fee_config │ │
|
|
453
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
454
|
+
│ │
|
|
455
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
456
|
+
│ │ 工具层 dubhe::entity_id / bcs / type_info / math │ │
|
|
457
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
458
|
+
└─────────────────────────────────────────────────────────────┘
|
|
459
|
+
│
|
|
460
|
+
▼
|
|
461
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
462
|
+
│ 链下索引器 / SDK(@0xobelisk/sui-sdk) │
|
|
463
|
+
│ · 订阅 Dubhe_Store_SetRecord 事件,实时同步状态 │
|
|
464
|
+
│ · 调用 get_record / get_field 读取链上数据 │
|
|
465
|
+
└─────────────────────────────────────────────────────────────┘
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
### DappHub:统一存储中心
|
|
471
|
+
|
|
472
|
+
Framework 部署时会通过 `init()` 将一个 **`DappHub`** 对象发布为全局共享对象(`share_object`)。链上只存在一个 DappHub,所有用 Dubhe 构建的 DApp 都把数据写入这同一个对象。
|
|
473
|
+
|
|
474
|
+
```
|
|
475
|
+
DappHub(全局共享对象)
|
|
476
|
+
└── accounts: ObjectTable<AccountKey, AccountData>
|
|
477
|
+
├── AccountKey { account: "0xABCD...", dapp_key: "my_project::dapp_key::DappKey" }
|
|
478
|
+
│ └── AccountData(dynamic_field 容器)
|
|
479
|
+
│ ├── key=["health"] → value=[BCS(100u32)]
|
|
480
|
+
│ └── key=["stats"] → value=[BCS(50u32), BCS(200u32)]
|
|
481
|
+
├── AccountKey { account: "0xABCD...", dapp_key: "another_game::dapp_key::DappKey" }
|
|
482
|
+
│ └── AccountData
|
|
483
|
+
│ └── key=["level"] → value=[BCS(5u32)]
|
|
484
|
+
└── ...
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**存储键的构成:** 每条记录由 `(resource_account, dapp_key_type_name)` 二元组定位到唯一的 `AccountData` 容器,再由 `[TABLE_NAME, ...extra_key_bytes]` 向量定位到容器内的具体字段。这个设计天然隔离了不同 DApp 的数据,即使它们共用同一个 DappHub。
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
### DappKey:类型级 DApp 身份证
|
|
492
|
+
|
|
493
|
+
`generate` 生成的 `dapp_key.move` 中定义了一个空结构体:
|
|
494
|
+
|
|
495
|
+
```move
|
|
496
|
+
public struct DappKey has copy, drop {}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
这个类型是你的 DApp 在 Framework 中的唯一身份标识。所有写入和读取操作都以 `DappKey` 的完整类型名(`package_id::module::DappKey`)作为命名空间前缀,确保:
|
|
500
|
+
|
|
501
|
+
- 不同 DApp 之间的数据完全隔离,不会发生冲突
|
|
502
|
+
- `package_id()` 方法直接从类型名中提取当前包地址,用于 Global 资源的 `resource_account` 定位
|
|
503
|
+
- 写入函数标注 `(package)` 可见性,外部包无法伪造 `DappKey` 实例越权写入
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
### 生成的 Resource 模块是如何调用 Framework 的
|
|
508
|
+
|
|
509
|
+
以一个简单的 `health: 'u32'` 配置为例,`generate` 会生成 `health.move`,其核心写入路径为:
|
|
510
|
+
|
|
511
|
+
```
|
|
512
|
+
health::set(dapp_hub, resource_account, 100u32, ctx)
|
|
513
|
+
│
|
|
514
|
+
├─ 1. 编码:encode(100u32) → vector<vector<u8>>
|
|
515
|
+
│
|
|
516
|
+
├─ 2. 调用 dapp_system::set_record(dapp_hub, DappKey{}, ["health"], [[100u32_bcs]], account, false, ctx)
|
|
517
|
+
│ │
|
|
518
|
+
│ ├─ 2a. dapp_service::set_record(...) ← 实际写入 DappHub dynamic_field
|
|
519
|
+
│ │ └─ 发射 Dubhe_Store_SetRecord 事件
|
|
520
|
+
│ │
|
|
521
|
+
│ └─ 2b. charge_fee(...) ← 按字节数扣除存储积分
|
|
522
|
+
│
|
|
523
|
+
└─ 完成
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
读取路径则直接调用 `dapp_service::get_record / get_field`(不经过 `dapp_system`,不产生费用),返回 BCS 字节后在生成代码中反序列化为 Move 原生类型。
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
### Framework 的五大赋能能力
|
|
531
|
+
|
|
532
|
+
#### 1. 零基础设施开发:统一存储
|
|
533
|
+
|
|
534
|
+
开发者无需自己定义 Sui 对象、管理 `UID`、处理 `ObjectTable`。Framework 提供了开箱即用的键值存储,生成的 resource 模块只需传入 `&mut DappHub` 即可完成读写,大幅降低了 Move 合约的开发门槛。
|
|
535
|
+
|
|
536
|
+
#### 2. 实时链下同步:事件驱动
|
|
537
|
+
|
|
538
|
+
每次调用 `set_record` 或 `delete_record`,Framework 都会自动发射结构化事件:
|
|
539
|
+
|
|
540
|
+
```move
|
|
541
|
+
public struct Dubhe_Store_SetRecord has copy, drop {
|
|
542
|
+
dapp_key: String, // 哪个 DApp
|
|
543
|
+
account: String, // 哪个账户
|
|
544
|
+
key: vector<vector<u8>>, // 哪张表
|
|
545
|
+
value: vector<vector<u8>>, // 新的值
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
SDK 的实时订阅功能正是基于这些事件,让链下应用(游戏前端、数据看板、索引器)可以近乎实时地感知链上状态变化,无需轮询。
|
|
550
|
+
|
|
551
|
+
#### 3. 内置 DApp 生命周期管理
|
|
552
|
+
|
|
553
|
+
通过 `genesis.move` 中的 `run()` 入口,合约部署后会自动在 DappHub 中注册当前 DApp,写入:
|
|
554
|
+
|
|
555
|
+
- **DappMetadata**:名称、描述、管理员地址、当前版本、已授权的 package_id 列表
|
|
556
|
+
- **DappFeeState**:初始化存储积分,记录累计写入字节数、操作次数等运营数据
|
|
557
|
+
|
|
558
|
+
`migrate.move` 中的版本常量配合 Framework 的 `ensure_latest_version()` 检查,提供了安全的合约升级路径:旧版本的 package 无法继续写入,强制用户迁移到最新版本。
|
|
559
|
+
|
|
560
|
+
#### 4. 存储费用计量系统
|
|
561
|
+
|
|
562
|
+
Framework 的 `dapp_system::set_record` 包含自动费用计量逻辑。每次写入按以下公式计算并扣除积分:
|
|
563
|
+
|
|
564
|
+
```
|
|
565
|
+
费用 = (写入总字节数 × byte_fee + base_fee) × 操作次数
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
积分优先从免费额度(`free_credit`,新 DApp 注册时赠送)扣除,耗尽后从充值余额扣除。这一机制让 Dubhe 能够可持续地维护链上基础设施,同时通过免费额度降低早期开发者的成本。
|
|
569
|
+
|
|
570
|
+
#### 5. Offchain 模式:极低成本的数据发布
|
|
571
|
+
|
|
572
|
+
将 resource 配置为 `offchain: true` 时,Framework 会跳过 `ObjectTable` 写入,只发射事件。数据由链下索引器接收并存入数据库。这对高频更新(如游戏实时状态、日志)场景极为有价值——避免了链上存储的高昂 Gas 成本,同时保持了区块链的可审计性。
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
### 部署流程与 Framework 交互时序
|
|
577
|
+
|
|
578
|
+
```
|
|
579
|
+
开发者执行 dubhe publish
|
|
580
|
+
│
|
|
581
|
+
▼
|
|
582
|
+
1. Sui 网络部署你的 Move 包
|
|
583
|
+
│
|
|
584
|
+
▼
|
|
585
|
+
2. genesis::run(dapp_hub, clock, ctx) 被调用
|
|
586
|
+
│
|
|
587
|
+
├─ 2a. dapp_system::create_dapp(dapp_hub, DappKey{}, name, description, clock, ctx)
|
|
588
|
+
│ ├─ dapp_metadata::set(...) ← 注册 DApp 元数据到 DappHub
|
|
589
|
+
│ └─ dapp_fee_state::set(...) ← 初始化存储积分账户
|
|
590
|
+
│
|
|
591
|
+
└─ 2b. deploy_hook::run(dapp_hub, ctx)
|
|
592
|
+
└─ 你的自定义初始化逻辑(如初始化全局配置、铸造初始资产等)
|
|
593
|
+
│
|
|
594
|
+
▼
|
|
595
|
+
3. DApp 上线,resource 模块开始提供读写服务
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
### 数据存储结构详解
|
|
601
|
+
|
|
602
|
+
以下以 `player_stats`(keyed 多字段资源)为例,展示完整的链上存储布局:
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
// DubheConfig
|
|
606
|
+
resources: {
|
|
607
|
+
player_stats: {
|
|
608
|
+
fields: { player: 'address', attack: 'u32', hp: 'u32' },
|
|
609
|
+
keys: ['player']
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
生成代码调用 `set(dapp_hub, resource_account, player_addr, 50u32, 200u32, ctx)` 后,DappHub 内部结构为:
|
|
615
|
+
|
|
616
|
+
```
|
|
617
|
+
DappHub.accounts
|
|
618
|
+
└── AccountKey {
|
|
619
|
+
account: resource_account, // 调用方传入的实体地址(如玩家钱包地址)
|
|
620
|
+
dapp_key: "0xPKG::dapp_key::DappKey"
|
|
621
|
+
}
|
|
622
|
+
└── AccountData.dynamic_fields
|
|
623
|
+
└── key = [b"player_stats", BCS(player_addr)]
|
|
624
|
+
↑ ↑
|
|
625
|
+
TABLE_NAME keys 字段的 BCS 编码
|
|
626
|
+
value = [BCS(50u32), BCS(200u32)]
|
|
627
|
+
↑ ↑
|
|
628
|
+
attack hp(非 key 的值字段,按声明顺序)
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
`get_field` 通过字段下标(`field_index: u8`)精确读取单个字段,避免反序列化整条记录,实现高效的局部更新。
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## 配置文件加载机制
|
|
636
|
+
|
|
637
|
+
执行 `dubhe generate` 或 `dubhe publish` 时,CLI 会调用 `loadConfig(configPath?)`,流程如下:
|
|
638
|
+
|
|
639
|
+
1. 从当前目录向上查找以下文件(按优先级排序):`dubhe.config.js`、`dubhe.config.mjs`、`dubhe.config.ts`、`dubhe.config.mts`
|
|
640
|
+
2. 使用 **esbuild** 将 TypeScript 配置编译为 ESM(打包本地 import,外部化 node_modules)
|
|
641
|
+
3. 动态 import 编译结果,返回其中的 `dubheConfig` 导出
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## 其他导出
|
|
646
|
+
|
|
647
|
+
### `parseData(data)`
|
|
648
|
+
|
|
649
|
+
递归地将链上原始 BCS 响应对象转换为普通 JavaScript 对象,标准化 Sui 的 `{ variant, fields }` 包装格式(枚举和结构体结果使用此格式)。
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
import { parseData } from '@0xobelisk/sui-common';
|
|
653
|
+
const result = parseData(rawOnChainObject);
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### `SubscriptionKind`
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
import { SubscriptionKind } from '@0xobelisk/sui-common';
|
|
660
|
+
// SubscriptionKind.Event | SubscriptionKind.Schema
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
SDK 用来区分实时订阅目标类型(事件 vs Schema 变更)。
|
|
664
|
+
|
|
665
|
+
### `defineConfig`
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
import { defineConfig } from '@0xobelisk/sui-common';
|
|
669
|
+
export const dubheConfig = defineConfig({ ... });
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
一个轻量的恒等包装函数,为 `DubheConfig` 提供 TypeScript 类型检查。每个 `dubhe.config.ts` 都应该使用它。
|