@0xobelisk/sui-common 1.2.0-pre.12 → 1.2.0-pre.120

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.
Files changed (41) hide show
  1. package/README.md +670 -1
  2. package/dist/index.d.ts +77 -45
  3. package/dist/index.js +1205 -204
  4. package/dist/index.js.map +1 -1
  5. package/package.json +20 -22
  6. package/src/codegen/debug.ts +0 -4
  7. package/src/codegen/types/index.ts +60 -73
  8. package/src/codegen/utils/config.ts +1 -1
  9. package/src/codegen/utils/format.ts +0 -6
  10. package/src/codegen/utils/formatAndWrite.ts +11 -32
  11. package/src/codegen/utils/generateLock.ts +122 -0
  12. package/src/codegen/utils/index.ts +5 -2
  13. package/src/codegen/utils/renderMove/codegen.ts +103 -0
  14. package/src/codegen/utils/renderMove/common.ts +0 -64
  15. package/src/codegen/utils/renderMove/dapp.ts +5 -0
  16. package/src/codegen/utils/renderMove/generateDappKey.ts +26 -12
  17. package/src/codegen/utils/renderMove/generateEnums.ts +57 -0
  18. package/src/codegen/utils/renderMove/generateError.ts +30 -26
  19. package/src/codegen/utils/renderMove/generateGenesis.ts +64 -0
  20. package/src/codegen/utils/renderMove/generateInitTest.ts +37 -0
  21. package/src/codegen/utils/renderMove/generateObjects.ts +373 -0
  22. package/src/codegen/utils/renderMove/generatePermits.ts +148 -0
  23. package/src/codegen/utils/renderMove/generateResources.ts +1471 -0
  24. package/src/codegen/utils/renderMove/generateScenes.ts +454 -0
  25. package/src/codegen/utils/renderMove/generateScript.ts +19 -15
  26. package/src/codegen/utils/renderMove/generateSystem.ts +0 -2
  27. package/src/codegen/utils/renderMove/generateToml.ts +2 -6
  28. package/src/codegen/utils/renderMove/generateUserStorageInit.ts +33 -0
  29. package/src/codegen/utils/validateConfig.ts +222 -0
  30. package/src/index.ts +0 -1
  31. package/src/modules.d.ts +0 -10
  32. package/src/codegen/modules.d.ts +0 -1
  33. package/src/codegen/utils/posixPath.ts +0 -8
  34. package/src/codegen/utils/renderMove/generateDefaultSchema.ts +0 -216
  35. package/src/codegen/utils/renderMove/generateEvent.ts +0 -98
  36. package/src/codegen/utils/renderMove/generateInit.ts +0 -79
  37. package/src/codegen/utils/renderMove/generateSchema.ts +0 -274
  38. package/src/codegen/utils/renderMove/generateSchemaHub.ts +0 -62
  39. package/src/codegen/utils/renderMove/schemaGen.ts +0 -65
  40. package/src/parseData/index.ts +0 -1
  41. package/src/parseData/parser/index.ts +0 -47
package/README.md CHANGED
@@ -1,3 +1,672 @@
1
1
  # @0xobelisk/sui-common
2
2
 
3
- Core code files being used for [@0xobelisk/sui-cli](../sui-cli/README.md)
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` 都应该使用它。