@0xobelisk/sui-common 1.2.0-pre.99 → 2.0.0

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 (39) hide show
  1. package/LICENSE +92 -0
  2. package/README.md +670 -1
  3. package/dist/index.d.ts +61 -27
  4. package/dist/index.js +1078 -461
  5. package/dist/index.js.map +1 -1
  6. package/package.json +15 -17
  7. package/src/codegen/debug.ts +0 -4
  8. package/src/codegen/types/index.ts +46 -10
  9. package/src/codegen/utils/config.ts +1 -1
  10. package/src/codegen/utils/format.ts +0 -6
  11. package/src/codegen/utils/formatAndWrite.ts +10 -31
  12. package/src/codegen/utils/generateLock.ts +122 -0
  13. package/src/codegen/utils/index.ts +4 -2
  14. package/src/codegen/utils/renderMove/{schemaGen.ts → codegen.ts} +40 -28
  15. package/src/codegen/utils/renderMove/common.ts +0 -65
  16. package/src/codegen/utils/renderMove/dapp.ts +2 -14
  17. package/src/codegen/utils/renderMove/generateDappKey.ts +33 -18
  18. package/src/codegen/utils/renderMove/generateError.ts +32 -15
  19. package/src/codegen/utils/renderMove/generateGenesis.ts +55 -22
  20. package/src/codegen/utils/renderMove/generateInitTest.ts +26 -14
  21. package/src/codegen/utils/renderMove/generateObjects.ts +377 -0
  22. package/src/codegen/utils/renderMove/generatePermits.ts +151 -0
  23. package/src/codegen/utils/renderMove/generateResources.ts +894 -242
  24. package/src/codegen/utils/renderMove/generateScenes.ts +467 -0
  25. package/src/codegen/utils/renderMove/generateScript.ts +18 -13
  26. package/src/codegen/utils/renderMove/generateSystem.ts +0 -2
  27. package/src/codegen/utils/renderMove/generateUserStorageInit.ts +37 -0
  28. package/src/codegen/utils/validateConfig.ts +237 -0
  29. package/src/index.ts +0 -1
  30. package/src/modules.d.ts +0 -10
  31. package/src/codegen/modules.d.ts +0 -1
  32. package/src/codegen/utils/posixPath.ts +0 -8
  33. package/src/codegen/utils/renderMove/generateComponents.ts +0 -802
  34. package/src/codegen/utils/renderMove/generateDefaultSchema.ts +0 -216
  35. package/src/codegen/utils/renderMove/generateEvent.ts +0 -99
  36. package/src/codegen/utils/renderMove/generateSchema.ts +0 -287
  37. package/src/codegen/utils/renderMove/generateSchemaHub.ts +0 -60
  38. package/src/parseData/index.ts +0 -1
  39. package/src/parseData/parser/index.ts +0 -47
@@ -1,24 +1,41 @@
1
- import { ErrorData } from '../../types';
1
+ import { ErrorData, ErrorEntry } from '../../types';
2
2
  import { formatAndWriteMove } from '../formatAndWrite';
3
3
 
4
- export async function generateSchemaError(projectName: string, errors: ErrorData, path: string) {
5
- console.log('\nšŸ“¦ Starting Schema Error Generation...');
4
+ function toErrorConstName(key: string): string {
5
+ return (
6
+ 'E' +
7
+ key
8
+ .split('_')
9
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
10
+ .join('')
11
+ );
12
+ }
13
+
14
+ function getMessage(entry: ErrorEntry): string {
15
+ return typeof entry === 'string' ? entry : entry.message;
16
+ }
17
+
18
+ export async function generateError(projectName: string, errors: ErrorData, path: string) {
19
+ console.log('\nšŸ“¦ Starting Error Generation...');
20
+
21
+ const entries = Object.entries(errors)
22
+ .map(([name, entry]) => {
23
+ const constName = toErrorConstName(name);
24
+ const message = getMessage(entry as ErrorEntry);
25
+ console.log(` └─ ${name}: ${message}`);
26
+ return [
27
+ ` #[error]`,
28
+ ` const ${constName}: vector<u8> = b"${message}";`,
29
+ ` public fun ${name}(condition: bool) { assert!(condition, ${constName}) }`
30
+ ].join('\n');
31
+ })
32
+ .join('\n\n');
6
33
 
7
- let code = `module ${projectName}::errors {
8
- ${Object.entries(errors)
9
- .map(([name, message]) => {
10
- console.log(` └─ ${name}: ${message}`);
11
- return `#[error]
12
- const ${name.toUpperCase()}: vector<u8> = b"${message}";
13
- public fun ${name}_error(condition: bool) { assert!(condition, ${name.toUpperCase()}) }
14
- `;
15
- })
16
- .join('\n')}
17
- }`;
34
+ const code = `module ${projectName}::error {\n${entries}\n}\n`;
18
35
 
19
36
  await formatAndWriteMove(
20
37
  code,
21
- `${path}/src/${projectName}/sources/codegen/errors.move`,
38
+ `${path}/src/${projectName}/sources/codegen/error.move`,
22
39
  'formatAndWriteMove'
23
40
  );
24
41
  }
@@ -1,30 +1,63 @@
1
1
  import { DubheConfig } from '../../types';
2
2
  import { formatAndWriteMove } from '../formatAndWrite';
3
- // import { existsSync } from 'fs'; // Unused
4
- // import { capitalizeAndRemoveUnderscores } from './generateSchema'; // Unused
5
- // import path from 'node:path'; // Unused
6
3
 
7
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
- function capitalizeFirstLetter(str: string): string {
9
- return str.charAt(0).toUpperCase() + str.slice(1);
4
+ export async function generateGenesis(config: DubheConfig, path: string, initialMode: 0 | 1 = 1) {
5
+ // The dubhe framework itself is not a DApp — it provides the infrastructure.
6
+ // Its genesis only needs to call deploy_hook::run to initialise framework state
7
+ // (fee config, admin, etc.) without creating a DappStorage for itself.
8
+ const isDubheFramework = config.name === 'dubhe';
9
+
10
+ let genesis_code = isDubheFramework
11
+ ? `module ${config.name}::genesis {
12
+ use dubhe::dapp_service::DappHub;
13
+
14
+ // The framework genesis initialises the DappHub state via deploy_hook.
15
+ // No DappStorage is created for the framework itself — the framework is
16
+ // infrastructure, not a DApp.
17
+ public fun run(dapp_hub: &mut DappHub, ctx: &mut TxContext) {
18
+ ${config.name}::deploy_hook::run(dapp_hub, ctx);
19
+ }
20
+
21
+ // Called during framework upgrades to run any custom migration logic.
22
+ // \`dubhe upgrade\` rewrites the region between the separator comments.
23
+ public(package) fun migrate(_dapp_hub: &mut DappHub, _ctx: &mut TxContext) {
24
+ // ==========================================
25
+ // Add custom migration logic here.
26
+ // ==========================================
27
+ }
10
28
  }
29
+ `
30
+ : `module ${config.name}::genesis {
31
+ use sui::clock::Clock;
32
+ use dubhe::dapp_service::{DappHub, DappStorage};
33
+ use ${config.name}::dapp_key;
34
+ use dubhe::dapp_system;
35
+ use std::ascii::string;
36
+
37
+ // The one-shot guard is enforced inside dapp_system::create_dapp, which
38
+ // records the DappKey type in DappHub before returning DappStorage.
39
+ // genesis.move does not need to carry its own guard.
40
+ public fun run(dapp_hub: &mut DappHub, clock: &Clock, ctx: &mut TxContext) {
41
+ // create_dapp aborts with dapp_already_initialized_error on repeated calls.
42
+ let dapp_key = dapp_key::new();
43
+ let mut ds = dapp_system::create_dapp(dapp_key, dapp_hub, string(b"${config.name}"), string(b"${config.description}"), ${initialMode}, clock, ctx);
44
+
45
+ // Set up initial DApp state (e.g. default resource values).
46
+ ${config.name}::deploy_hook::run(&mut ds, ctx);
47
+
48
+ // Share DappStorage so every transaction can access it.
49
+ transfer::public_share_object(ds);
50
+ }
11
51
 
12
- export async function generateGenesis(config: DubheConfig, path: string) {
13
- let genesis_code = `module ${config.name}::genesis {
14
- use sui::clock::Clock;
15
- use dubhe::dapp_service::DappHub;
16
- use ${config.name}::dapp_key;
17
- use dubhe::dapp_system;
18
- use std::ascii::string;
19
-
20
- public entry fun run(dapp_hub: &mut DappHub, clock: &Clock, ctx: &mut TxContext) {
21
- // Create Dapp
22
- let dapp_key = dapp_key::new();
23
- dapp_system::create_dapp(dapp_hub, dapp_key, string(b"${config.name}"), string(b"${config.description}"), clock, ctx);
24
-
25
- // Logic that needs to be automated once the contract is deployed
26
- ${config.name}::deploy_hook::run(dapp_hub, ctx);
27
- }
52
+ // Called during contract upgrades to register newly added resource tables
53
+ // and run any custom migration logic. \`dubhe upgrade\` rewrites the region
54
+ // between the separator comments; do not edit that block manually.
55
+ public(package) fun migrate(_dapp_hub: &mut DappHub, _dapp_storage: &mut DappStorage, _ctx: &mut TxContext) {
56
+ // ==========================================
57
+ // Add custom migration logic here (e.g. initialise new resource defaults).
58
+ // migrate_to_vN in migrate.move calls this function automatically.
59
+ // ==========================================
60
+ }
28
61
  }
29
62
  `;
30
63
  await formatAndWriteMove(genesis_code, path, 'formatAndWriteMove');
@@ -2,23 +2,35 @@ import { DubheConfig } from '../../types';
2
2
  import { formatAndWriteMove } from '../formatAndWrite';
3
3
 
4
4
  export async function generateInitTest(config: DubheConfig, path: string) {
5
- let init_test_code = `module ${config.name}::init_test {
6
- use sui::clock;
7
- use sui::test_scenario;
8
- use sui::test_scenario::Scenario;
9
- use dubhe::dapp_service::DappHub;
5
+ // For the dubhe package itself, we only need DappHub helpers (no UserStorage/DappStorage per-DApp).
6
+ // For DApp packages, expose all three test helpers.
7
+ const isDubhe = config.name === 'dubhe';
8
+
9
+ const dappSpecificHelpers = !isDubhe
10
+ ? `
11
+ /// Create a DappStorage for this DApp without sharing it.
12
+ /// Suitable for unit tests that exercise global-resource functions.
13
+ public fun create_dapp_storage_for_testing(ctx: &mut TxContext): dubhe::dapp_service::DappStorage {
14
+ dubhe::dapp_system::create_dapp_storage_for_testing<${config.name}::dapp_key::DappKey>(ctx)
15
+ }
16
+
17
+ /// Create a UserStorage for \`owner\` without sharing it.
18
+ /// Suitable for unit tests that exercise user-level resource functions.
19
+ public fun create_user_storage_for_testing(owner: address, ctx: &mut TxContext): dubhe::dapp_service::UserStorage {
20
+ dubhe::dapp_system::create_user_storage_for_testing<${config.name}::dapp_key::DappKey>(owner, ctx)
21
+ }`
22
+ : '';
23
+
24
+ let init_test_code = `
25
+ module ${config.name}::init_test {
10
26
  use dubhe::dapp_system;
11
27
 
12
- public fun deploy_dapp_for_testing(scenario: &mut Scenario): DappHub {
13
- let ctx = test_scenario::ctx(scenario);
14
- let clock = clock::create_for_testing(ctx);
15
- let mut dapp_hub = dapp_system::create_dapp_hub_for_testing(ctx);
16
- ${config.name != 'dubhe' ? `dubhe::genesis::run(&mut dapp_hub, &clock, ctx);` : ''}
17
- ${config.name}::genesis::run(&mut dapp_hub, &clock, ctx);
18
- clock::destroy_for_testing(clock);
19
- test_scenario::next_tx(scenario, ctx.sender());
20
- dapp_hub
28
+ /// Create a DappHub for testing without sharing it.
29
+ /// Suitable for unit tests that need a DappHub context.
30
+ public fun create_dapp_hub_for_testing(ctx: &mut TxContext): dubhe::dapp_service::DappHub {
31
+ dapp_system::create_dapp_hub_for_testing(ctx)
21
32
  }
33
+ ${dappSpecificHelpers}
22
34
  }
23
35
  `;
24
36
  await formatAndWriteMove(init_test_code, path, 'formatAndWriteMove');
@@ -0,0 +1,377 @@
1
+ import { DubheConfig, ObjectConfig, Component } from '../../types';
2
+ import { formatAndWriteMove } from '../formatAndWrite';
3
+ import path from 'node:path';
4
+
5
+ function toPascalCase(str: string): string {
6
+ return str
7
+ .split('_')
8
+ .map((word) => {
9
+ if (/^\d+$/.test(word)) return word;
10
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
11
+ })
12
+ .join('');
13
+ }
14
+
15
+ function getMoveType(t: string): string {
16
+ return t === 'string' || t === 'String' ? 'String' : t;
17
+ }
18
+
19
+ /**
20
+ * Generate typed field accessors (get/set) for an ObjectStorage's own fields.
21
+ * Calls the framework's set_object_field / get_object_field which handle
22
+ * native-type storage and event emission automatically.
23
+ */
24
+ // Type token for substitution into generated code
25
+ function objStorageType(markerName: string): string {
26
+ return `dubhe::dapp_service::ObjectStorage<${markerName}>`;
27
+ }
28
+
29
+ function generateFieldAccessors(objKey: string, cfg: ObjectConfig): string {
30
+ const markerName = toPascalCase(objKey);
31
+ const storageType = objStorageType(markerName);
32
+ const lines: string[] = [];
33
+
34
+ for (const [fieldName, fieldType] of Object.entries(cfg.fields)) {
35
+ const moveType = getMoveType(fieldType as string);
36
+
37
+ lines.push(`
38
+ public fun get_${fieldName}(storage: &${storageType}): ${moveType} {
39
+ dubhe::dapp_system::get_object_field<${markerName}, ${moveType}>(storage, b"${fieldName}")
40
+ }
41
+
42
+ public(package) fun set_${fieldName}(storage: &mut ${storageType}, value: ${moveType}) {
43
+ dubhe::dapp_system::set_object_field<DappKey, ${markerName}, ${moveType}>(
44
+ dapp_key::new(), storage, b"${fieldName}", value
45
+ );
46
+ }`);
47
+ }
48
+
49
+ return lines.join('\n');
50
+ }
51
+
52
+ /**
53
+ * Generate bag accessor functions for a fungible resource accepted by this object.
54
+ * Reads use has_object_field to return a default of 0; writes do a read-modify-write
55
+ * through the framework CRUD API so events are emitted.
56
+ */
57
+ function generateFungibleBagAccessors(objKey: string, resourceName: string): string {
58
+ const markerName = toPascalCase(objKey);
59
+ const storageType = objStorageType(markerName);
60
+
61
+ return `
62
+ public fun get_${resourceName}(storage: &${storageType}): u64 {
63
+ if (dubhe::dapp_system::has_object_field<${markerName}, u64>(storage, b"${resourceName}")) {
64
+ dubhe::dapp_system::get_object_field<${markerName}, u64>(storage, b"${resourceName}")
65
+ } else { 0 }
66
+ }
67
+
68
+ public(package) fun add_${resourceName}(storage: &mut ${storageType}, amount: u64) {
69
+ let current = get_${resourceName}(storage);
70
+ dubhe::dapp_system::set_object_field<DappKey, ${markerName}, u64>(
71
+ dapp_key::new(), storage, b"${resourceName}", current + amount
72
+ );
73
+ }
74
+
75
+ public(package) fun sub_${resourceName}(storage: &mut ${storageType}, amount: u64) {
76
+ let current = get_${resourceName}(storage);
77
+ assert!(current >= amount, EInsufficientAmount);
78
+ dubhe::dapp_system::set_object_field<DappKey, ${markerName}, u64>(
79
+ dapp_key::new(), storage, b"${resourceName}", current - amount
80
+ );
81
+ }`;
82
+ }
83
+
84
+ /**
85
+ * Generate bag accessor functions for a keyed resource accepted by this object.
86
+ * item_id is BCS-encoded as the Bag key; value is vector<u8> (raw bytes).
87
+ */
88
+ function generateKeyedBagAccessors(objKey: string, resourceName: string, idField: string): string {
89
+ const markerName = toPascalCase(objKey);
90
+ const storageType = objStorageType(markerName);
91
+
92
+ return `
93
+ public fun has_${resourceName}(storage: &${storageType}, ${idField}: u64): bool {
94
+ let key = sui::bcs::to_bytes(&${idField});
95
+ dubhe::dapp_system::has_object_field<${markerName}, vector<u8>>(storage, key)
96
+ }
97
+
98
+ public fun get_${resourceName}_data(storage: &${storageType}, ${idField}: u64): vector<u8> {
99
+ let key = sui::bcs::to_bytes(&${idField});
100
+ dubhe::dapp_system::get_object_field<${markerName}, vector<u8>>(storage, key)
101
+ }
102
+
103
+ public(package) fun set_${resourceName}_data(storage: &mut ${storageType}, ${idField}: u64, data: vector<u8>) {
104
+ let key = sui::bcs::to_bytes(&${idField});
105
+ assert!(!dubhe::dapp_system::has_object_field<${markerName}, vector<u8>>(storage, key), EDuplicateItemId);
106
+ dubhe::dapp_system::set_object_field<DappKey, ${markerName}, vector<u8>>(
107
+ dapp_key::new(), storage, key, data
108
+ );
109
+ }
110
+
111
+ public(package) fun remove_${resourceName}_data(storage: &mut ${storageType}, ${idField}: u64): vector<u8> {
112
+ let key = sui::bcs::to_bytes(&${idField});
113
+ assert!(dubhe::dapp_system::has_object_field<${markerName}, vector<u8>>(storage, key), EFieldNotFound);
114
+ dubhe::dapp_system::remove_object_field<DappKey, ${markerName}, vector<u8>>(dapp_key::new(), storage, key)
115
+ }`;
116
+ }
117
+
118
+ /**
119
+ * Generate cross-storage transfer functions for acceptsFrom sources.
120
+ * These live in the DESTINATION module and atomically move resources between
121
+ * two ObjectStorage (or SceneStorage) objects.
122
+ */
123
+ function generateAcceptsFromTransfers(
124
+ projectName: string,
125
+ destKey: string,
126
+ destAccepts: string[],
127
+ acceptsFrom: string[],
128
+ config: DubheConfig
129
+ ): { imports: string[]; functions: string[] } {
130
+ const resources = config.resources ?? {};
131
+ const allObjects = config.objects ?? {};
132
+ const allScenes = config.scenes ?? {};
133
+
134
+ const destMarker = toPascalCase(destKey);
135
+ const destStorageType = objStorageType(destMarker);
136
+ const imports: string[] = [];
137
+ const functions: string[] = [];
138
+
139
+ for (const sourceName of acceptsFrom) {
140
+ const sourceCfg = allObjects[sourceName] ?? allScenes[sourceName];
141
+ if (!sourceCfg) continue;
142
+
143
+ const sourceAccepts = sourceCfg.accepts ?? [];
144
+ const sourceMarker = toPascalCase(sourceName);
145
+ // Source may be ObjectStorage or SceneStorage — import just the module
146
+ imports.push(` use ${projectName}::${sourceName};`);
147
+
148
+ const commonResources = sourceAccepts.filter((r) => destAccepts.includes(r));
149
+
150
+ for (const resourceName of commonResources) {
151
+ const resCfg = resources[resourceName];
152
+ if (!resCfg || typeof resCfg === 'string') continue;
153
+ const comp = resCfg as Component;
154
+
155
+ // Use fully-qualified phantom type to avoid import issues
156
+ const isSourceScene = !!allScenes[sourceName];
157
+ const qualifiedSourceMarker = `${projectName}::${sourceName}::${sourceMarker}`;
158
+ const sourceStorageType = isSourceScene
159
+ ? `dubhe::dapp_service::SceneStorage<${qualifiedSourceMarker}>`
160
+ : `dubhe::dapp_service::ObjectStorage<${qualifiedSourceMarker}>`;
161
+ const sourceSceneCfg = isSourceScene ? allScenes[sourceName] : undefined;
162
+ const sourcePermitType =
163
+ sourceSceneCfg?.authorization.kind === 'permit'
164
+ ? `dubhe::dapp_service::ScenePermit<${projectName}::${
165
+ sourceSceneCfg.authorization.permit
166
+ }::${toPascalCase(sourceSceneCfg.authorization.permit)}>`
167
+ : '';
168
+ const sourcePermitParam =
169
+ sourceSceneCfg?.authorization.kind === 'permit'
170
+ ? ` source_permit: &${sourcePermitType},\n`
171
+ : '';
172
+ const sourceIsPermitScene = sourceSceneCfg?.authorization.kind === 'permit';
173
+ const sourceCallPrefix = sourceIsPermitScene ? 'source_permit, ' : '';
174
+ // Permit-authorized scene writes resolve the caller identity from user_storage.
175
+ const sourceUserStorageParam = sourceIsPermitScene
176
+ ? ` user_storage: &dubhe::dapp_service::UserStorage,\n`
177
+ : '';
178
+ const sourceUserStorageCall = sourceIsPermitScene ? 'user_storage, ' : '';
179
+ const sourceCtxParam = sourceIsPermitScene ? ' ctx: &TxContext,\n' : '';
180
+ const sourceCtxCall = sourceIsPermitScene ? ', ctx' : '';
181
+
182
+ if (!comp.fungible && comp.keys?.length) {
183
+ const idField = comp.keys[0];
184
+ functions.push(`
185
+ /// Transfer ${resourceName} (keyed item) from ${sourceName} into this ${destKey}.
186
+ public(package) fun transfer_${sourceName}_to_${destKey}_${resourceName}(
187
+ ${sourcePermitParam} from: &mut ${sourceStorageType},
188
+ to: &mut ${destStorageType},
189
+ ${sourceUserStorageParam} ${idField}: u64,
190
+ ${sourceCtxParam}
191
+ ) {
192
+ let data = ${sourceName}::remove_${resourceName}_data(${sourceCallPrefix}from, ${sourceUserStorageCall}${idField}${sourceCtxCall});
193
+ set_${resourceName}_data(to, ${idField}, data);
194
+ }`);
195
+ } else {
196
+ functions.push(`
197
+ /// Transfer ${resourceName} (fungible) from ${sourceName} into this ${destKey}.
198
+ public(package) fun transfer_${sourceName}_to_${destKey}_${resourceName}(
199
+ ${sourcePermitParam} from: &mut ${sourceStorageType},
200
+ to: &mut ${destStorageType},
201
+ ${sourceUserStorageParam} amount: u64,
202
+ ${sourceCtxParam}
203
+ ) {
204
+ ${sourceName}::sub_${resourceName}(${sourceCallPrefix}from, ${sourceUserStorageCall}amount${sourceCtxCall});
205
+ add_${resourceName}(to, amount);
206
+ }`);
207
+ }
208
+ }
209
+ }
210
+
211
+ return { imports, functions };
212
+ }
213
+
214
+ export async function generateObjects(config: DubheConfig, outputDir: string) {
215
+ if (!config.objects || Object.keys(config.objects).length === 0) return;
216
+ console.log('\nšŸ“¦ Starting Object Storage Generation...');
217
+
218
+ const projectName = config.name;
219
+ const resources = config.resources ?? {};
220
+
221
+ for (const [objKey, objCfg] of Object.entries(config.objects)) {
222
+ console.log(` └─ ${objKey}`);
223
+ const markerName = toPascalCase(objKey);
224
+ const storageAlias = `${markerName}Storage`;
225
+ const typeTag = `b"${objKey}"`;
226
+
227
+ // Own field accessors (framework CRUD calls)
228
+ const fieldAccessors = generateFieldAccessors(objKey, objCfg);
229
+
230
+ // Bag accessors for accepted resources
231
+ const acceptedResources = objCfg.accepts ?? [];
232
+ const bagAccessorParts: string[] = [];
233
+ for (const resourceName of acceptedResources) {
234
+ const resCfg = resources[resourceName];
235
+ if (!resCfg || typeof resCfg === 'string') continue;
236
+ const comp = resCfg as Component;
237
+ if (!comp.fungible && comp.keys?.length) {
238
+ bagAccessorParts.push(generateKeyedBagAccessors(objKey, resourceName, comp.keys[0]));
239
+ } else {
240
+ bagAccessorParts.push(generateFungibleBagAccessors(objKey, resourceName));
241
+ }
242
+ }
243
+
244
+ // acceptsFrom: cross-storage transfer functions
245
+ const { imports: afImports, functions: afFunctions } = generateAcceptsFromTransfers(
246
+ projectName,
247
+ objKey,
248
+ acceptedResources,
249
+ objCfg.acceptsFrom ?? [],
250
+ config
251
+ );
252
+
253
+ // adminOnly check for create
254
+ const adminCheck = objCfg.adminOnly
255
+ ? ` assert!(ctx.sender() == dubhe::dapp_service::dapp_admin(dapp_storage), ENoPermission);`
256
+ : '';
257
+
258
+ const fullStorageTypeLocal = `dubhe::dapp_service::ObjectStorage<${markerName}>`;
259
+
260
+ // assert_<key>_id helper (reads entity_id via framework accessor)
261
+ const assertIdFn = `
262
+ public fun assert_${objKey}_id(storage: &${fullStorageTypeLocal}, expected: vector<u8>) {
263
+ assert!(*dubhe::dapp_service::object_storage_entity_id(storage) == expected, EWrongEntityId);
264
+ }`;
265
+
266
+ // entity_id accessor
267
+ const entityIdFn = `
268
+ public fun entity_id(storage: &${fullStorageTypeLocal}): vector<u8> {
269
+ *dubhe::dapp_service::object_storage_entity_id(storage)
270
+ }`;
271
+
272
+ // Conditional String import: needed when own fields or String-typed accepts are used
273
+ const ownFieldTypes = Object.values(objCfg.fields) as string[];
274
+ const needsStringImport = ownFieldTypes.some(
275
+ (t) => t === 'string' || t === 'String' || t === 'vector<String>'
276
+ );
277
+ const stringImport = needsStringImport ? `\n use std::ascii::String;` : '';
278
+
279
+ // Conditional error constants
280
+ const hasKeyedAccepts = acceptedResources.some((r) => {
281
+ const rc = resources[r];
282
+ if (!rc || typeof rc === 'string') return false;
283
+ return !!(rc as Component).keys?.length && !(rc as Component).fungible;
284
+ });
285
+ const hasFungibleAccepts = acceptedResources.some((r) => {
286
+ const rc = resources[r];
287
+ if (!rc || typeof rc === 'string') return false;
288
+ return !!(rc as Component).fungible;
289
+ });
290
+ // EFieldNotFound is only used in remove_*_data for keyed bag accessors
291
+ const objNeedsFieldNotFound = hasKeyedAccepts;
292
+ const objNeedsInsufficient = hasFungibleAccepts;
293
+ const objNeedsDuplicate = hasKeyedAccepts;
294
+ const objNeedsNoPermission = !!objCfg.adminOnly;
295
+
296
+ const errorConstants = [
297
+ objNeedsFieldNotFound
298
+ ? ` #[error]\n const EFieldNotFound: vector<u8> = b"Field not found";`
299
+ : '',
300
+ objNeedsInsufficient
301
+ ? ` #[error]\n const EInsufficientAmount: vector<u8> = b"Insufficient amount";`
302
+ : '',
303
+ objNeedsDuplicate
304
+ ? ` #[error]\n const EDuplicateItemId: vector<u8> = b"Duplicate item id";`
305
+ : '',
306
+ ` #[error]\n const EWrongEntityId: vector<u8> = b"Wrong entity id";`,
307
+ objNeedsNoPermission
308
+ ? ` #[error]\n const ENoPermission: vector<u8> = b"Caller does not have permission";`
309
+ : ''
310
+ ]
311
+ .filter(Boolean)
312
+ .join('\n');
313
+
314
+ const afImportBlock = afImports.length > 0 ? '\n' + afImports.join('\n') : '';
315
+
316
+ // Use the full framework type name for all function signatures.
317
+ // Move type aliases (public type) are not yet supported in this Sui version,
318
+ // so we use ObjectStorage<Marker> directly.
319
+ const fullStorageType = `dubhe::dapp_service::ObjectStorage<${markerName}>`;
320
+
321
+ const code = `module ${projectName}::${objKey} {
322
+ use dubhe::dapp_service::DappStorage;
323
+ use ${projectName}::dapp_key;
324
+ use ${projectName}::dapp_key::DappKey;${stringImport}${afImportBlock}
325
+
326
+ // ─── Error constants ───────────────────────────────────────────────────
327
+ ${errorConstants}
328
+
329
+ const TYPE_TAG: vector<u8> = ${typeTag};
330
+
331
+ // ─── Phantom marker type ───────────────────────────────────────────────
332
+ /// Phantom type that distinguishes ${storageAlias} from other ObjectStorage types
333
+ /// at the Move compiler level, preserving compile-time type safety.
334
+ /// All functions use ObjectStorage<${markerName}> directly in their signatures.
335
+ public struct ${markerName} has copy, drop {}
336
+
337
+ // ─── ID / entity accessors ─────────────────────────────────────────────
338
+ ${entityIdFn}
339
+
340
+ ${assertIdFn}
341
+
342
+ // ─── Field accessors (own fields) ──────────────────────────────────────
343
+ ${fieldAccessors}
344
+
345
+ // ─── Bag accessors for accepted resources ─────────────────────────────
346
+ ${bagAccessorParts.join('\n')}
347
+
348
+ // ─── acceptsFrom: cross-storage transfer functions ─────────────────────
349
+ ${afFunctions.join('\n')}
350
+
351
+ // ─── Lifecycle entry functions ─────────────────────────────────────────
352
+ public(package) fun create_${objKey}(
353
+ dapp_storage: &mut DappStorage,
354
+ entity_id: vector<u8>,
355
+ ctx: &mut TxContext,
356
+ ) {
357
+ ${adminCheck}
358
+ dubhe::dapp_system::create_and_share_typed_object<DappKey, ${markerName}>(
359
+ dapp_key::new(), dapp_storage, TYPE_TAG, entity_id, ctx
360
+ );
361
+ }
362
+
363
+ public(package) fun destroy_${objKey}(
364
+ dapp_storage: &mut DappStorage,
365
+ storage: ${fullStorageType},
366
+ _ctx: &TxContext,
367
+ ) {
368
+ dubhe::dapp_system::destroy_typed_object<DappKey, ${markerName}>(
369
+ dapp_key::new(), dapp_storage, storage
370
+ );
371
+ }
372
+ }
373
+ `;
374
+
375
+ await formatAndWriteMove(code, path.join(outputDir, `${objKey}.move`), 'formatAndWriteMove');
376
+ }
377
+ }