@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,14 +1,74 @@
1
- import { DubheConfig } from '../../types';
2
- import { ComponentType } from '../../types';
1
+ import { DubheConfig, ComponentType, Component } from '../../types';
3
2
  import { formatAndWriteMove } from '../formatAndWrite';
4
3
 
4
+ // For the dubhe framework package itself, use package-internal dapp_service functions.
5
+ // For DApp packages, use the public dapp_system API.
5
6
  function getDappModuleName(projectName: string): string {
6
7
  return projectName === 'dubhe' ? 'dapp_service' : `dapp_system`;
7
8
  }
8
9
 
10
+ // Returns the inline auth argument for dapp_system write functions.
11
+ // dapp_system::set_record / set_field / delete_record / delete_field all require a DappKey
12
+ // value as the first argument so that only code inside the DApp's own package (where
13
+ // dapp_key::new() is public(package)) can supply it, preventing external bypass.
14
+ // dapp_service functions (used by dubhe's own code) have no such parameter.
15
+ function authArg(projectName: string): string {
16
+ return projectName !== 'dubhe' ? 'dapp_key::new(), ' : '';
17
+ }
18
+
19
+ // dapp_system::set_record, set_field, set_global_record, and set_global_field no longer
20
+ // require &DappHub — fee rates and debt ceilings are hardcoded as constants in dapp_system
21
+ // and updated via package upgrade. These helpers always return empty strings.
22
+ function dappHubArg(_projectName: string, _isGlobal: boolean): string {
23
+ return '';
24
+ }
25
+
26
+ function dappHubParam(_projectName: string, _isGlobal: boolean): string {
27
+ return '';
28
+ }
29
+
30
+ // Returns the Move storage object type based on whether the resource is global.
31
+ function getStorageType(isGlobal: boolean): string {
32
+ return isGlobal ? 'DappStorage' : 'UserStorage';
33
+ }
34
+
35
+ // Returns the storage param name used in generated function signatures.
36
+ function getStorageParamName(isGlobal: boolean): string {
37
+ return isGlobal ? 'dapp_storage' : 'user_storage';
38
+ }
39
+
40
+ // Returns the correct record-access function names based on global flag and project.
41
+ function getRecordFns(projectName: string, isGlobal: boolean) {
42
+ const mod = getDappModuleName(projectName);
43
+ if (isGlobal) {
44
+ return {
45
+ set_record: `${mod}::set_global_record`,
46
+ set_field: `${mod}::set_global_field`,
47
+ get_field: `${mod}::get_global_field`,
48
+ has_record: `${mod}::has_global_record`,
49
+ ensure_has: `${mod}::ensure_has_global_record`,
50
+ ensure_has_not: `${mod}::ensure_has_not_global_record`,
51
+ delete_record: `${mod}::delete_global_record`,
52
+ delete_field: `${mod}::delete_global_field`
53
+ };
54
+ }
55
+ return {
56
+ set_record: `${mod}::set_record`,
57
+ set_field: `${mod}::set_field`,
58
+ get_field: `${mod}::get_field`,
59
+ has_record: `${mod}::has_record`,
60
+ ensure_has: `${mod}::ensure_has_record`,
61
+ ensure_has_not: `${mod}::ensure_has_not_record`,
62
+ delete_record: `${mod}::delete_record`,
63
+ delete_field: `${mod}::delete_field`
64
+ };
65
+ }
66
+
9
67
  export async function generateResources(config: DubheConfig, path: string) {
10
68
  console.log('\n📦 Starting Resources Generation...');
11
69
 
70
+ if (!config.resources) return;
71
+
12
72
  for (const [componentName, resource] of Object.entries(config.resources)) {
13
73
  console.log(` └─ ${componentName}: ${JSON.stringify(resource)}`);
14
74
 
@@ -31,7 +91,16 @@ export async function generateResources(config: DubheConfig, path: string) {
31
91
  resource.keys = [];
32
92
  }
33
93
 
34
- const code = generateComponentCode(config.name, componentName, resource);
94
+ const baseCode = generateComponentCode(config.name, componentName, resource);
95
+ const extensionCode = generateAnnotationExtensions(
96
+ config,
97
+ componentName,
98
+ resource as Component
99
+ );
100
+
101
+ // Merge extension functions into the base module
102
+ const code = extensionCode ? baseCode.replace(/^}$/m, `\n${extensionCode}\n}`) : baseCode;
103
+
35
104
  await formatAndWriteMove(code, `${path}/${componentName}.move`, 'formatAndWriteMove');
36
105
  }
37
106
  }
@@ -42,81 +111,106 @@ function generateSimpleComponentCode(
42
111
  valueType: string,
43
112
  type: ComponentType = 'Onchain'
44
113
  ): string {
45
- // Check if it's an enum type
46
114
  const isEnum = !isBasicType(valueType);
47
115
  const enumModule = isEnum ? `${toSnakeCase(valueType)}` : '';
48
116
  const isOffchain = type === 'Offchain';
117
+ // Simple types default to global=false (user storage)
118
+ const isGlobal = false;
119
+ const storageType = getStorageType(isGlobal);
120
+ const storageParam = getStorageParamName(isGlobal);
121
+ const fns = getRecordFns(projectName, isGlobal);
49
122
 
50
- // For offchain resources, only generate set and encode functions
51
123
  const readFunctions = !isOffchain
52
124
  ? `
53
- public fun has(dapp_hub: &DappHub, resource_account: String): bool {
125
+ public fun has(${storageParam}: &${storageType}): bool {
54
126
  let mut key_tuple = vector::empty();
55
127
  key_tuple.push_back(TABLE_NAME);
56
- ${getDappModuleName(
57
- projectName
58
- )}::has_record<DappKey>(dapp_hub, resource_account, key_tuple)
128
+ ${fns.has_record}<DappKey>(${storageParam}, key_tuple)
59
129
  }
60
130
 
61
- public(package) fun delete(dapp_hub: &mut DappHub, resource_account: String) {
131
+ public fun ensure_has(${storageParam}: &${storageType}) {
62
132
  let mut key_tuple = vector::empty();
63
133
  key_tuple.push_back(TABLE_NAME);
64
- ${getDappModuleName(
65
- projectName
66
- )}::delete_record<DappKey>(dapp_hub, dapp_key::new(), key_tuple, resource_account);
134
+ ${fns.ensure_has}<DappKey>(${storageParam}, key_tuple)
67
135
  }
68
136
 
69
- public fun get(dapp_hub: &DappHub, resource_account: String): (${
70
- valueType === 'string' || valueType === 'String' ? 'String' : valueType
71
- }) {
137
+ public fun ensure_has_not(${storageParam}: &${storageType}) {
72
138
  let mut key_tuple = vector::empty();
73
139
  key_tuple.push_back(TABLE_NAME);
74
- let value_tuple = ${getDappModuleName(
75
- projectName
76
- )}::get_record<DappKey>(dapp_hub, resource_account, key_tuple);
77
- let mut bsc_type = sui::bcs::new(value_tuple);
78
- ${
79
- valueType === 'string' || valueType === 'String'
80
- ? `let value = dubhe::bcs::peel_string(&mut bsc_type);`
81
- : valueType === 'vector<String>'
82
- ? `let value = dubhe::bcs::peel_vec_string(&mut bsc_type);`
83
- : isEnum
84
- ? `let value = ${projectName}::${enumModule}::decode(&mut bsc_type);`
85
- : `let value = sui::bcs::peel_${getBcsType(valueType)}(&mut bsc_type);`
86
- }
140
+ ${fns.ensure_has_not}<DappKey>(${storageParam}, key_tuple)
141
+ }
142
+
143
+ public(package) fun delete(${storageParam}: &mut ${storageType}, ctx: &TxContext) {
144
+ let mut key_tuple = vector::empty();
145
+ key_tuple.push_back(TABLE_NAME);
146
+ ${fns.delete_record}<DappKey>(${authArg(
147
+ projectName
148
+ )}${storageParam}, key_tuple, vector[b"value"], ctx);
149
+ }
150
+
151
+ public fun get(${storageParam}: &${storageType}): (${
152
+ valueType === 'string' || valueType === 'String' ? 'String' : valueType
153
+ }) {
154
+ let mut key_tuple = vector::empty();
155
+ key_tuple.push_back(TABLE_NAME);
156
+ let value_raw = ${fns.get_field}<DappKey>(${storageParam}, key_tuple, b"value");
157
+ let mut value_bcs = sui::bcs::new(value_raw);
158
+ let value = ${buildParseExpr(
159
+ projectName,
160
+ valueType,
161
+ 'value_bcs',
162
+ isEnum ? [{ type: valueType, module: enumModule }] : []
163
+ )};
87
164
  (value)
88
165
  }
89
166
  `
90
167
  : '';
91
168
 
92
- return `module ${projectName}::${componentName} {
93
- use sui::bcs::{to_bytes};
94
- use std::ascii::{string, String, into_bytes};
95
- use dubhe::table_id;
96
- use dubhe::dapp_service::{Self, DappHub};
169
+ const storageImport = isGlobal
170
+ ? `use dubhe::dapp_service::DappStorage;`
171
+ : `use dubhe::dapp_service::UserStorage;`;
172
+
173
+ // Imports are conditional on the value type:
174
+ // - to_bytes: needed for basic types and vectors (not for enum types)
175
+ // - std::ascii:
176
+ // String type: {String, into_bytes} (string fn unused - decode uses fully qualified path)
177
+ // vector<String>: {String} (into_bytes fn unused - encode uses fully qualified path)
178
+ // enum: nothing
179
+ const isEnumType = isEnum && valueType !== 'string' && valueType !== 'String';
180
+ const needsToBytesImport = !isEnumType;
181
+ const toBytesImport = needsToBytesImport ? `\n use sui::bcs::{to_bytes};` : '';
182
+ const asciiImport =
183
+ valueType === 'string' || valueType === 'String'
184
+ ? `\n use std::ascii::{String, into_bytes};`
185
+ : valueType === 'vector<String>'
186
+ ? `\n use std::ascii::String;`
187
+ : '';
188
+
189
+ return `module ${projectName}::${componentName} {${toBytesImport}${asciiImport}
190
+ ${storageImport}
97
191
  use dubhe::dapp_system;
98
192
  use ${projectName}::dapp_key;
99
193
  use ${projectName}::dapp_key::DappKey;
100
- ${
101
- isEnum && valueType !== 'string' && valueType !== 'String'
102
- ? ` use ${projectName}::${enumModule};
103
- use ${projectName}::${enumModule}::{${valueType}};`
104
- : ''
105
- }
194
+ ${isEnumType ? ` use ${projectName}::${enumModule}::{${valueType}};` : ''}
106
195
 
107
196
  const TABLE_NAME: vector<u8> = b"${componentName}";
108
197
  const OFFCHAIN: bool = ${type === 'Offchain'};
109
198
 
110
199
  ${readFunctions}
111
- public(package) fun set(dapp_hub: &mut DappHub, resource_account: String, value: ${
112
- valueType === 'string' || valueType === 'String' ? 'String' : valueType
113
- }, ctx: &mut TxContext) {
200
+ public(package) fun set(${dappHubParam(
201
+ projectName,
202
+ isGlobal
203
+ )}${storageParam}: &mut ${storageType}, value: ${
204
+ valueType === 'string' || valueType === 'String' ? 'String' : valueType
205
+ }, ctx: &mut TxContext) {
114
206
  let mut key_tuple = vector::empty();
115
207
  key_tuple.push_back(TABLE_NAME);
208
+ let field_names = vector[b"value"];
116
209
  let value_tuple = encode(value);
117
- ${getDappModuleName(
118
- projectName
119
- )}::set_record(dapp_hub, dapp_key::new(), key_tuple, value_tuple, resource_account, OFFCHAIN, ctx);
210
+ ${fns.set_record}<DappKey>(${authArg(projectName)}${dappHubArg(
211
+ projectName,
212
+ isGlobal
213
+ )}${storageParam}, key_tuple, field_names, value_tuple, OFFCHAIN, ctx);
120
214
  }
121
215
 
122
216
  public fun encode(value: ${
@@ -145,11 +239,13 @@ function generateComponentCode(projectName: string, componentName: string, resou
145
239
  const fields = resource.fields;
146
240
  const keys = resource.keys || [];
147
241
  const offchain = resource.offchain || false;
148
- const global = (resource as any).global || false;
242
+ const global = resource.global || false;
149
243
  const type: ComponentType = offchain ? 'Offchain' : 'Onchain';
150
244
 
151
- // Check if it's global: offchain=true OR global=true
152
- const isGlobal = offchain || global;
245
+ const isGlobal = global;
246
+ const storageType = getStorageType(isGlobal);
247
+ const storageParam = getStorageParamName(isGlobal);
248
+ const fns = getRecordFns(projectName, isGlobal);
153
249
 
154
250
  // Check if all fields are keys
155
251
  const isAllKeys = Object.keys(fields).every((name) => keys.includes(name));
@@ -161,7 +257,7 @@ function generateComponentCode(projectName: string, componentName: string, resou
161
257
  // If there is only one value field, do not generate struct
162
258
  const isSingleValue = valueFieldNames.length === 1;
163
259
 
164
- // Get all enum type fields and their corresponding module names
260
+ // Enum types from value fields only (used for encode/decode logic)
165
261
  const enumTypes = valueFields
166
262
  .filter(([_, type]) => !isBasicType(type as string) && type !== 'string' && type !== 'String')
167
263
  .map(([_, type]) => ({
@@ -170,6 +266,15 @@ function generateComponentCode(projectName: string, componentName: string, resou
170
266
  }))
171
267
  .filter((item, index, self) => self.findIndex((t) => t.type === item.type) === index);
172
268
 
269
+ // All enum types (key + value) for import statement generation
270
+ const allEnumTypes = Object.entries(fields)
271
+ .filter(([_, type]) => !isBasicType(type as string) && type !== 'string' && type !== 'String')
272
+ .map(([_, type]) => ({
273
+ type: type as string,
274
+ module: `${toSnakeCase(type as string)}`
275
+ }))
276
+ .filter((item, index, self) => self.findIndex((t) => t.type === item.type) === index);
277
+
173
278
  // Generate table related functions
174
279
  const tableFunctions = generateTableFunctions(
175
280
  projectName,
@@ -179,27 +284,62 @@ function generateComponentCode(projectName: string, componentName: string, resou
179
284
  !isAllKeys && !isSingleValue,
180
285
  enumTypes,
181
286
  type,
182
- isGlobal
287
+ isGlobal,
288
+ storageType,
289
+ storageParam,
290
+ fns
183
291
  );
184
292
 
293
+ // listable buy() still takes dapp_storage: &mut DappStorage; reactive no longer does
294
+ const storageImport = isGlobal
295
+ ? `use dubhe::dapp_service::DappStorage;`
296
+ : resource.listable
297
+ ? `use dubhe::dapp_service::{UserStorage, DappStorage};`
298
+ : `use dubhe::dapp_service::UserStorage;`;
299
+ const scenePermitImport = resource.reactive ? `\n use dubhe::dapp_service::ScenePermit;` : '';
300
+
301
+ // Determine if any field uses ascii String type (controls std::ascii import)
302
+ const allFieldTypes = Object.values(fields) as string[];
303
+ // to_bytes needed when any value field is NOT a pure enum type
304
+ const nonEnumValueFields = valueFieldNames.filter((name) => {
305
+ const t = fields[name] as string;
306
+ return isBasicType(t) || t === 'string' || t === 'String';
307
+ });
308
+ const needsToBytesImport = nonEnumValueFields.length > 0 || keys.length > 0;
309
+ const toBytesImport = needsToBytesImport ? `\n use sui::bcs::{to_bytes};` : '';
310
+ // String type needed when any field is string/String/vector<String>
311
+ const hasStringField = allFieldTypes.some((t) => t === 'string' || t === 'String');
312
+ const hasVecStringField = allFieldTypes.some((t) => t === 'vector<String>');
313
+ // Whether String appears in value fields (not just key fields)
314
+ const hasStringValueField = valueFieldNames.some((n) => {
315
+ const t = fields[n] as string;
316
+ return t === 'string' || t === 'String';
317
+ });
318
+ // Single-value / all-keys path: into_bytes only needed when value field is String.
319
+ // Key encoding uses to_bytes(&key) directly (String is BCS-serialisable).
320
+ // Decode uses dubhe::bcs::peel_string (fully qualified), no 'string' fn alias needed.
321
+ const asciiImportSimple = hasStringValueField
322
+ ? `\n use std::ascii::{String, into_bytes};`
323
+ : hasStringField || hasVecStringField
324
+ ? `\n use std::ascii::String;`
325
+ : '';
326
+ // Multi-field struct path: decode calls string(peel_vec_u8(...)) so 'string' fn alias needed.
327
+ const asciiImportMulti = hasStringValueField
328
+ ? `\n use std::ascii::{string, String, into_bytes};`
329
+ : hasStringField || hasVecStringField
330
+ ? `\n use std::ascii::String;`
331
+ : '';
332
+
185
333
  // If all fields are keys or there is only one value field, do not generate struct related code
186
334
  if (isAllKeys || isSingleValue) {
187
- return `module ${projectName}::${componentName} {
188
- use sui::bcs::{to_bytes};
189
- use std::ascii::{string, String, into_bytes};
190
- use dubhe::table_id;
191
- use dubhe::dapp_service::{Self, DappHub};
335
+ return `module ${projectName}::${componentName} {${toBytesImport}${asciiImportSimple}
336
+ ${storageImport}${scenePermitImport}
192
337
  use dubhe::dapp_system;
193
338
  use ${projectName}::dapp_key;
194
339
  use ${projectName}::dapp_key::DappKey;
195
340
  ${
196
- enumTypes.length > 0
197
- ? enumTypes
198
- .map(
199
- (e) => ` use ${projectName}::${e.module};
200
- use ${projectName}::${e.module}::{${e.type}};`
201
- )
202
- .join('\n')
341
+ allEnumTypes.length > 0
342
+ ? allEnumTypes.map((e) => ` use ${projectName}::${e.module}::{${e.type}};`).join('\n')
203
343
  : ''
204
344
  }
205
345
 
@@ -273,22 +413,14 @@ ${tableFunctions}
273
413
  )
274
414
  .join('\n\n');
275
415
 
276
- return `module ${projectName}::${componentName} {
277
- use sui::bcs::{to_bytes};
278
- use std::ascii::{string, String, into_bytes};
279
- use dubhe::table_id;
280
- use dubhe::dapp_service::{Self, DappHub};
416
+ return `module ${projectName}::${componentName} {${toBytesImport}${asciiImportMulti}
417
+ ${storageImport}${scenePermitImport}
281
418
  use dubhe::dapp_system;
282
419
  use ${projectName}::dapp_key;
283
420
  use ${projectName}::dapp_key::DappKey;
284
421
  ${
285
- enumTypes.length > 0
286
- ? enumTypes
287
- .map(
288
- (e) => ` use ${projectName}::${e.module};
289
- use ${projectName}::${e.module}::{${e.type}};`
290
- )
291
- .join('\n')
422
+ allEnumTypes.length > 0
423
+ ? allEnumTypes.map((e) => ` use ${projectName}::${e.module}::{${e.type}};`).join('\n')
292
424
  : ''
293
425
  }
294
426
 
@@ -319,6 +451,7 @@ function isBasicType(type: string): boolean {
319
451
  'address',
320
452
  'bool',
321
453
  'u8',
454
+ 'u16',
322
455
  'u32',
323
456
  'u64',
324
457
  'u128',
@@ -328,6 +461,7 @@ function isBasicType(type: string): boolean {
328
461
  'vector<address>',
329
462
  'vector<bool>',
330
463
  'vector<u8>',
464
+ 'vector<u16>',
331
465
  'vector<vector<u8>>',
332
466
  'vector<u32>',
333
467
  'vector<u64>',
@@ -337,6 +471,47 @@ function isBasicType(type: string): boolean {
337
471
  ].includes(type);
338
472
  }
339
473
 
474
+ // Build a BCS peel expression for a single field, reading from bcsVar.
475
+ function buildParseExpr(
476
+ projectName: string,
477
+ fieldType: string,
478
+ bcsVar: string,
479
+ enumTypes: Array<{ type: string; module: string }>
480
+ ): string {
481
+ const isEnum = !isBasicType(fieldType);
482
+ const enumType = isEnum ? enumTypes.find((e) => e.type === fieldType) : null;
483
+ if (fieldType === 'string' || fieldType === 'String') {
484
+ return `dubhe::bcs::peel_string(&mut ${bcsVar})`;
485
+ }
486
+ if (fieldType === 'vector<String>') {
487
+ return `dubhe::bcs::peel_vec_string(&mut ${bcsVar})`;
488
+ }
489
+ if (isEnum && enumType) {
490
+ return `${projectName}::${enumType.module}::decode(&mut ${bcsVar})`;
491
+ }
492
+ return `sui::bcs::peel_${getBcsType(fieldType)}(&mut ${bcsVar})`;
493
+ }
494
+
495
+ // Generate the three-line sequence that reads one named field from storage.
496
+ // Uses ${fieldName}_raw / ${fieldName}_bcs as readable temporary variable names.
497
+ function buildFieldReadLines(
498
+ projectName: string,
499
+ fns: ReturnType<typeof getRecordFns>,
500
+ storageParam: string,
501
+ fieldName: string,
502
+ fieldType: string,
503
+ _idx: number,
504
+ enumTypes: Array<{ type: string; module: string }>
505
+ ): string {
506
+ const rawVar = `${fieldName}_raw`;
507
+ const bcsVar = `${fieldName}_bcs`;
508
+ return [
509
+ `let ${rawVar} = ${fns.get_field}<DappKey>(${storageParam}, key_tuple, b"${fieldName}");`,
510
+ `let mut ${bcsVar} = sui::bcs::new(${rawVar});`,
511
+ `let ${fieldName} = ${buildParseExpr(projectName, fieldType, bcsVar, enumTypes)};`
512
+ ].join('\n ');
513
+ }
514
+
340
515
  function generateTableFunctions(
341
516
  projectName: string,
342
517
  componentName: string,
@@ -345,7 +520,10 @@ function generateTableFunctions(
345
520
  includeStruct: boolean = true,
346
521
  enumTypes: Array<{ type: string; module: string }> = [],
347
522
  type: ComponentType = 'Onchain',
348
- isGlobal: boolean = false
523
+ isGlobal: boolean = false,
524
+ storageType: string = 'UserStorage',
525
+ storageParam: string = 'user_storage',
526
+ fns: ReturnType<typeof getRecordFns> = getRecordFns('dubhe', false)
349
527
  ): string {
350
528
  // Separate key fields and non-key fields
351
529
  const valueFields = Object.entries(fields)
@@ -373,72 +551,62 @@ function generateTableFunctions(
373
551
  : `let mut key_tuple = vector::empty();
374
552
  key_tuple.push_back(TABLE_NAME);`;
375
553
 
376
- // For global resources, use package_id() as resource_account
377
- const resourceAccountParam = isGlobal ? '' : 'resource_account: String';
378
- const resourceAccountArg = isGlobal
379
- ? 'dapp_key::package_id().to_ascii_string()'
380
- : 'resource_account';
381
-
382
- // Add comma after resourceAccountParam only if both exist
383
- const paramSeparator = resourceAccountParam && keyParams ? ', ' : '';
554
+ // ctx helpers: global write functions (set_global_record, set_global_field) do not take
555
+ // a TxContext parameter fees are handled inside those functions without needing ctx.
556
+ // Non-global write functions (set_record, set_field, delete_record) DO take ctx.
557
+ const ctxParam = isGlobal ? '' : ', ctx: &mut TxContext';
558
+ const ctxArg = isGlobal ? '' : ', ctx';
384
559
 
385
560
  // Generate has series functions - skip for offchain
386
561
  const hasFunctions = !isOffchain
387
- ? ` public fun has(dapp_hub: &DappHub${
388
- resourceAccountParam || keyParams ? ', ' : ''
389
- }${resourceAccountParam}${paramSeparator}${keyParams}): bool {
562
+ ? ` public fun has(${storageParam}: &${storageType}${
563
+ keyParams ? ', ' : ''
564
+ }${keyParams}): bool {
390
565
  ${keyTupleCode}
391
- ${getDappModuleName(
392
- projectName
393
- )}::has_record<DappKey>(dapp_hub, ${resourceAccountArg}, key_tuple)
566
+ ${fns.has_record}<DappKey>(${storageParam}, key_tuple)
394
567
  }
395
568
 
396
- public fun ensure_has(dapp_hub: &DappHub${
397
- resourceAccountParam || keyParams ? ', ' : ''
398
- }${resourceAccountParam}${paramSeparator}${keyParams}) {
569
+ public fun ensure_has(${storageParam}: &${storageType}${keyParams ? ', ' : ''}${keyParams}) {
399
570
  ${keyTupleCode}
400
- ${getDappModuleName(
401
- projectName
402
- )}::ensure_has_record<DappKey>(dapp_hub, ${resourceAccountArg}, key_tuple)
571
+ ${fns.ensure_has}<DappKey>(${storageParam}, key_tuple)
403
572
  }
404
573
 
405
- public fun ensure_has_not(dapp_hub: &DappHub${
406
- resourceAccountParam || keyParams ? ', ' : ''
407
- }${resourceAccountParam}${paramSeparator}${keyParams}) {
574
+ public fun ensure_has_not(${storageParam}: &${storageType}${
575
+ keyParams ? ', ' : ''
576
+ }${keyParams}) {
408
577
  ${keyTupleCode}
409
- ${getDappModuleName(
410
- projectName
411
- )}::ensure_has_not_record<DappKey>(dapp_hub, ${resourceAccountArg}, key_tuple)
578
+ ${fns.ensure_has_not}<DappKey>(${storageParam}, key_tuple)
412
579
  }
413
580
  `
414
581
  : '';
415
582
 
416
- // Generate delete function - skip for offchain
583
+ // Generate delete function - skip for offchain.
584
+ // Pass all field names to delete_record so it cleans up fields and the sentinel in one call.
585
+ const fieldNamesLiteral = `vector[${valueNames.map((n) => `b"${n}"`).join(', ')}]`;
417
586
  const deleteFunction = !isOffchain
418
- ? ` public(package) fun delete(dapp_hub: &mut DappHub${
419
- resourceAccountParam || keyParams ? ', ' : ''
420
- }${resourceAccountParam}${paramSeparator}${keyParams}) {
587
+ ? ` public(package) fun delete(${storageParam}: &mut ${storageType}${
588
+ keyParams ? ', ' : ''
589
+ }${keyParams}${isGlobal ? '' : ', ctx: &TxContext'}) {
421
590
  ${keyTupleCode}
422
- ${getDappModuleName(
423
- projectName
424
- )}::delete_record<DappKey>(dapp_hub, dapp_key::new(), key_tuple, ${resourceAccountArg});
591
+ ${fns.delete_record}<DappKey>(${authArg(
592
+ projectName
593
+ )}${storageParam}, key_tuple, ${fieldNamesLiteral}${isGlobal ? '' : ', ctx'});
425
594
  }`
426
595
  : '';
427
596
 
428
- // Generate getter and setter functions, only generated when there are multiple value fields
429
- // For offchain, skip all getter and setter functions
597
+ // Generate individual getter / setter functions for each value field.
598
+ // Only generated when there are multiple value fields; skipped for offchain.
430
599
  const getterSetters =
431
600
  !isSingleValue && !isOffchain
432
601
  ? valueNames
433
602
  .map((name) => {
434
- const index = valueNames.indexOf(name);
435
603
  const fieldType = fields[name];
436
604
  const isEnum = !isBasicType(fieldType as string);
437
605
  const enumType = isEnum ? enumTypes.find((e) => e.type === fieldType) : null;
438
606
 
439
- return ` public fun get_${name}(dapp_hub: &DappHub${
440
- resourceAccountParam || keyParams ? ', ' : ''
441
- }${resourceAccountParam}${paramSeparator}${keyParams}): ${
607
+ return ` public fun get_${name}(${storageParam}: &${storageType}${
608
+ keyParams ? ', ' : ''
609
+ }${keyParams}): ${
442
610
  fieldType === 'string' || fieldType === 'String'
443
611
  ? 'String'
444
612
  : fieldType === 'vector<String>'
@@ -446,31 +614,22 @@ function generateTableFunctions(
446
614
  : fieldType
447
615
  } {
448
616
  ${keyTupleCode}
449
- let value = ${getDappModuleName(
450
- projectName
451
- )}::get_field<DappKey>(dapp_hub, ${resourceAccountArg}, key_tuple, ${index});
452
- let mut bsc_type = sui::bcs::new(value);
453
- ${
454
- fieldType === 'string' || fieldType === 'String'
455
- ? `let ${name} = dubhe::bcs::peel_string(&mut bsc_type);`
456
- : fieldType === 'vector<String>'
457
- ? `let ${name} = dubhe::bcs::peel_vec_string(&mut bsc_type);`
458
- : isEnum
459
- ? `let ${name} = ${projectName}::${enumType?.module}::decode(&mut bsc_type);`
460
- : `let ${name} = sui::bcs::peel_${getBcsType(fieldType)}(&mut bsc_type);`
461
- }
617
+ let ${name}_raw = ${fns.get_field}<DappKey>(${storageParam}, key_tuple, b"${name}");
618
+ let mut ${name}_bcs = sui::bcs::new(${name}_raw);
619
+ let ${name} = ${buildParseExpr(projectName, fieldType as string, `${name}_bcs`, enumTypes)};
462
620
  ${name}
463
621
  }
464
622
 
465
- public(package) fun set_${name}(dapp_hub: &mut DappHub${
466
- resourceAccountParam || keyParams ? ', ' : ''
467
- }${resourceAccountParam}${paramSeparator}${keyParams}, ${name}: ${
623
+ public(package) fun set_${name}(${dappHubParam(
624
+ projectName,
625
+ isGlobal
626
+ )}${storageParam}: &mut ${storageType}${keyParams ? ', ' : ''}${keyParams}, ${name}: ${
468
627
  fieldType === 'string' || fieldType === 'String'
469
628
  ? 'String'
470
629
  : fieldType === 'vector<String>'
471
630
  ? 'vector<String>'
472
631
  : fieldType
473
- }) {
632
+ }${ctxParam}) {
474
633
  ${keyTupleCode}
475
634
  let value = ${
476
635
  fieldType === 'string' || fieldType === 'String'
@@ -481,172 +640,177 @@ function generateTableFunctions(
481
640
  ? `${projectName}::${enumType?.module}::encode(${name})`
482
641
  : `to_bytes(&${name})`
483
642
  };
484
- ${getDappModuleName(
485
- projectName
486
- )}::set_field(dapp_hub, dapp_key::new(), key_tuple, ${index}, value, ${resourceAccountArg});
643
+ ${fns.set_field}<DappKey>(${authArg(projectName)}${dappHubArg(
644
+ projectName,
645
+ isGlobal
646
+ )}${storageParam}, key_tuple, b"${name}", value${ctxArg});
487
647
  }`;
488
648
  })
489
649
  .join('\n\n')
490
650
  : '';
491
651
 
652
+ // Build the field-name vector literal used in set_record calls.
653
+ const fieldNamesVec = `vector[${valueNames.map((n) => `b"${n}"`).join(', ')}]`;
654
+
492
655
  // Generate get and set functions
493
656
  const getSetFunctions = isAllKeys
494
- ? ` public(package) fun set(dapp_hub: &mut DappHub${
495
- resourceAccountParam || keyParams ? ', ' : ''
496
- }${resourceAccountParam}${paramSeparator}${keyParams}, ctx: &mut TxContext) {
657
+ ? ` public(package) fun set(${dappHubParam(
658
+ projectName,
659
+ isGlobal
660
+ )}${storageParam}: &mut ${storageType}${keyParams ? ', ' : ''}${keyParams}${ctxParam}) {
497
661
  ${keyTupleCode}
498
- let value_tuple = vector::empty();
499
- ${getDappModuleName(
500
- projectName
501
- )}::set_record(dapp_hub, dapp_key::new(), key_tuple, value_tuple, ${resourceAccountArg}, OFFCHAIN, ctx);
662
+ let field_names: vector<vector<u8>> = vector[];
663
+ let value_tuple: vector<vector<u8>> = vector[];
664
+ ${fns.set_record}<DappKey>(${authArg(projectName)}${dappHubArg(
665
+ projectName,
666
+ isGlobal
667
+ )}${storageParam}, key_tuple, field_names, value_tuple, OFFCHAIN${ctxArg});
502
668
  }`
503
669
  : isSingleValue
504
670
  ? !isOffchain
505
- ? ` public fun get(dapp_hub: &DappHub${
506
- resourceAccountParam || keyParams ? ', ' : ''
507
- }${resourceAccountParam}${paramSeparator}${keyParams}): ${
671
+ ? ` public fun get(${storageParam}: &${storageType}${
672
+ keyParams ? ', ' : ''
673
+ }${keyParams}): ${
508
674
  Object.values(valueFields)[0] === 'string' || Object.values(valueFields)[0] === 'String'
509
675
  ? 'String'
510
676
  : Object.values(valueFields)[0]
511
677
  } {
512
678
  ${keyTupleCode}
513
- let value = ${getDappModuleName(
514
- projectName
515
- )}::get_field<DappKey>(dapp_hub, ${resourceAccountArg}, key_tuple, 0);
516
- let mut bsc_type = sui::bcs::new(value);
517
- ${
518
- Object.values(valueFields)[0] === 'string' || Object.values(valueFields)[0] === 'String'
519
- ? `let value = dubhe::bcs::peel_string(&mut bsc_type);`
520
- : Object.values(valueFields)[0] === 'vector<String>'
521
- ? `let value = dubhe::bcs::peel_vec_string(&mut bsc_type);`
522
- : !isBasicType(Object.values(valueFields)[0] as string)
523
- ? `let value = ${projectName}::${
524
- enumTypes.find((e) => e.type === Object.values(valueFields)[0])?.module
525
- }::decode(&mut bsc_type);`
526
- : `let value = sui::bcs::peel_${getBcsType(
527
- Object.values(valueFields)[0] as string
528
- )}(&mut bsc_type);`
529
- }
679
+ let ${valueNames[0]}_raw = ${fns.get_field}<DappKey>(${storageParam}, key_tuple, b"${
680
+ valueNames[0]
681
+ }");
682
+ let mut ${valueNames[0]}_bcs = sui::bcs::new(${valueNames[0]}_raw);
683
+ let value = ${buildParseExpr(
684
+ projectName,
685
+ Object.values(valueFields)[0] as string,
686
+ `${valueNames[0]}_bcs`,
687
+ enumTypes
688
+ )};
530
689
  value
531
690
  }
532
691
 
533
- public(package) fun set(dapp_hub: &mut DappHub${
534
- resourceAccountParam || keyParams ? ', ' : ''
535
- }${resourceAccountParam}${paramSeparator}${keyParams}, value: ${
692
+ public(package) fun set(${dappHubParam(
693
+ projectName,
694
+ isGlobal
695
+ )}${storageParam}: &mut ${storageType}${keyParams ? ', ' : ''}${keyParams}, value: ${
536
696
  Object.values(valueFields)[0] === 'string' || Object.values(valueFields)[0] === 'String'
537
697
  ? 'String'
538
698
  : Object.values(valueFields)[0]
539
- }, ctx: &mut TxContext) {
699
+ }${ctxParam}) {
540
700
  ${keyTupleCode}
701
+ let field_names = vector[b"${valueNames[0]}"];
541
702
  let value_tuple = encode(value);
542
- ${getDappModuleName(
543
- projectName
544
- )}::set_record(dapp_hub, dapp_key::new(), key_tuple, value_tuple, ${resourceAccountArg}, OFFCHAIN, ctx);
703
+ ${fns.set_record}<DappKey>(${authArg(projectName)}${dappHubArg(
704
+ projectName,
705
+ isGlobal
706
+ )}${storageParam}, key_tuple, field_names, value_tuple, OFFCHAIN${ctxArg});
545
707
  }`
546
- : ` public(package) fun set(dapp_hub: &mut DappHub${
547
- resourceAccountParam || keyParams ? ', ' : ''
548
- }${resourceAccountParam}${paramSeparator}${keyParams}, value: ${
708
+ : ` public(package) fun set(${dappHubParam(
709
+ projectName,
710
+ isGlobal
711
+ )}${storageParam}: &mut ${storageType}${keyParams ? ', ' : ''}${keyParams}, value: ${
549
712
  Object.values(valueFields)[0] === 'string' || Object.values(valueFields)[0] === 'String'
550
713
  ? 'String'
551
714
  : Object.values(valueFields)[0]
552
- }, ctx: &mut TxContext) {
715
+ }${ctxParam}) {
553
716
  ${keyTupleCode}
717
+ let field_names = vector[b"${valueNames[0]}"];
554
718
  let value_tuple = encode(value);
555
- ${getDappModuleName(
556
- projectName
557
- )}::set_record(dapp_hub, dapp_key::new(), key_tuple, value_tuple, ${resourceAccountArg}, OFFCHAIN, ctx);
719
+ ${fns.set_record}<DappKey>(${authArg(projectName)}${dappHubArg(
720
+ projectName,
721
+ isGlobal
722
+ )}${storageParam}, key_tuple, field_names, value_tuple, OFFCHAIN${ctxArg});
558
723
  }`
559
724
  : !isOffchain
560
- ? ` public fun get(dapp_hub: &DappHub${
561
- resourceAccountParam || keyParams ? ', ' : ''
562
- }${resourceAccountParam}${paramSeparator}${keyParams}): (${Object.values(valueFields)
725
+ ? ` public fun get(${storageParam}: &${storageType}${
726
+ keyParams ? ', ' : ''
727
+ }${keyParams}): (${Object.values(valueFields)
563
728
  .map((t) => (t === 'string' || t === 'String' ? 'String' : t))
564
729
  .join(', ')}) {
565
730
  ${keyTupleCode}
566
- let value_tuple = ${getDappModuleName(
567
- projectName
568
- )}::get_record<DappKey>(dapp_hub, ${resourceAccountArg}, key_tuple);
569
- let mut bsc_type = sui::bcs::new(value_tuple);
570
731
  ${valueNames
571
- .map((name) => {
572
- const fieldType = fields[name];
573
- const isEnum = !isBasicType(fieldType as string);
574
- const enumType = isEnum ? enumTypes.find((e) => e.type === fieldType) : null;
575
- return `let ${name} = ${
576
- fieldType === 'string' || fieldType === 'String'
577
- ? `dubhe::bcs::peel_string(&mut bsc_type)`
578
- : fieldType === 'vector<String>'
579
- ? `dubhe::bcs::peel_vec_string(&mut bsc_type)`
580
- : isEnum
581
- ? `${projectName}::${enumType?.module}::decode(&mut bsc_type)`
582
- : `sui::bcs::peel_${getBcsType(fieldType)}(&mut bsc_type)`
583
- };`;
584
- })
732
+ .map((name, i) =>
733
+ buildFieldReadLines(projectName, fns, storageParam, name, fields[name], i, enumTypes)
734
+ )
585
735
  .join('\n ')}
586
736
  (${valueNames.join(', ')})
587
737
  }
588
738
 
589
- public(package) fun set(dapp_hub: &mut DappHub${
590
- resourceAccountParam || keyParams ? ', ' : ''
591
- }${resourceAccountParam}${paramSeparator}${keyParams}, ${valueNames
739
+ public(package) fun set(${dappHubParam(
740
+ projectName,
741
+ isGlobal
742
+ )}${storageParam}: &mut ${storageType}${keyParams ? ', ' : ''}${keyParams}, ${valueNames
592
743
  .map(
593
744
  (n) => `${n}: ${fields[n] === 'string' || fields[n] === 'String' ? 'String' : fields[n]}`
594
745
  )
595
- .join(', ')}, ctx: &mut TxContext) {
746
+ .join(', ')}${ctxParam}) {
596
747
  ${keyTupleCode}
748
+ let field_names = ${fieldNamesVec};
597
749
  let value_tuple = encode(${valueNames.join(', ')});
598
- ${getDappModuleName(
599
- projectName
600
- )}::set_record(dapp_hub, dapp_key::new(), key_tuple, value_tuple, ${resourceAccountArg}, OFFCHAIN, ctx);
750
+ ${fns.set_record}<DappKey>(${authArg(projectName)}${dappHubArg(
751
+ projectName,
752
+ isGlobal
753
+ )}${storageParam}, key_tuple, field_names, value_tuple, OFFCHAIN${ctxArg});
601
754
  }`
602
- : ` public(package) fun set(dapp_hub: &mut DappHub${
603
- resourceAccountParam || keyParams ? ', ' : ''
604
- }${resourceAccountParam}${paramSeparator}${keyParams}, ${valueNames
755
+ : ` public(package) fun set(${dappHubParam(
756
+ projectName,
757
+ isGlobal
758
+ )}${storageParam}: &mut ${storageType}${keyParams ? ', ' : ''}${keyParams}, ${valueNames
605
759
  .map(
606
760
  (n) => `${n}: ${fields[n] === 'string' || fields[n] === 'String' ? 'String' : fields[n]}`
607
761
  )
608
- .join(', ')}, ctx: &mut TxContext) {
762
+ .join(', ')}${ctxParam}) {
609
763
  ${keyTupleCode}
764
+ let field_names = ${fieldNamesVec};
610
765
  let value_tuple = encode(${valueNames.join(', ')});
611
- ${getDappModuleName(
612
- projectName
613
- )}::set_record(dapp_hub, dapp_key::new(), key_tuple, value_tuple, ${resourceAccountArg}, OFFCHAIN, ctx);
766
+ ${fns.set_record}<DappKey>(${authArg(projectName)}${dappHubArg(
767
+ projectName,
768
+ isGlobal
769
+ )}${storageParam}, key_tuple, field_names, value_tuple, OFFCHAIN${ctxArg});
614
770
  }`;
615
771
 
616
772
  // Generate struct related functions
617
773
  const structFunctions = includeStruct
618
774
  ? !isOffchain
619
- ? ` public fun get_struct(dapp_hub: &DappHub${
620
- resourceAccountParam || keyParams ? ', ' : ''
621
- }${resourceAccountParam}${paramSeparator}${keyParams}): ${toPascalCase(componentName)} {
775
+ ? ` public fun get_struct(${storageParam}: &${storageType}${
776
+ keyParams ? ', ' : ''
777
+ }${keyParams}): ${toPascalCase(componentName)} {
622
778
  ${keyTupleCode}
623
- let value_tuple = ${getDappModuleName(
624
- projectName
625
- )}::get_record<DappKey>(dapp_hub, ${resourceAccountArg}, key_tuple);
626
- decode(value_tuple)
779
+ ${valueNames
780
+ .map((name, i) =>
781
+ buildFieldReadLines(projectName, fns, storageParam, name, fields[name], i, enumTypes)
782
+ )
783
+ .join('\n ')}
784
+ ${toPascalCase(componentName)} { ${valueNames.join(', ')} }
627
785
  }
628
786
 
629
- public(package) fun set_struct(dapp_hub: &mut DappHub${
630
- resourceAccountParam || keyParams ? ', ' : ''
631
- }${resourceAccountParam}${paramSeparator}${keyParams}, ${componentName}: ${toPascalCase(
632
- componentName
633
- )}, ctx: &mut TxContext) {
787
+ public(package) fun set_struct(${dappHubParam(
788
+ projectName,
789
+ isGlobal
790
+ )}${storageParam}: &mut ${storageType}${
791
+ keyParams ? ', ' : ''
792
+ }${keyParams}, ${componentName}: ${toPascalCase(componentName)}${ctxParam}) {
634
793
  ${keyTupleCode}
794
+ let field_names = ${fieldNamesVec};
635
795
  let value_tuple = encode_struct(${componentName});
636
- ${getDappModuleName(
637
- projectName
638
- )}::set_record(dapp_hub, dapp_key::new(), key_tuple, value_tuple, ${resourceAccountArg}, OFFCHAIN, ctx);
796
+ ${fns.set_record}<DappKey>(${authArg(projectName)}${dappHubArg(
797
+ projectName,
798
+ isGlobal
799
+ )}${storageParam}, key_tuple, field_names, value_tuple, OFFCHAIN${ctxArg});
639
800
  }`
640
- : ` public(package) fun set_struct(dapp_hub: &mut DappHub${
641
- resourceAccountParam || keyParams ? ', ' : ''
642
- }${resourceAccountParam}${paramSeparator}${keyParams}, ${componentName}: ${toPascalCase(
643
- componentName
644
- )}, ctx: &mut TxContext) {
801
+ : ` public(package) fun set_struct(${dappHubParam(
802
+ projectName,
803
+ isGlobal
804
+ )}${storageParam}: &mut ${storageType}${
805
+ keyParams ? ', ' : ''
806
+ }${keyParams}, ${componentName}: ${toPascalCase(componentName)}${ctxParam}) {
645
807
  ${keyTupleCode}
808
+ let field_names = ${fieldNamesVec};
646
809
  let value_tuple = encode_struct(${componentName});
647
- ${getDappModuleName(
648
- projectName
649
- )}::set_record(dapp_hub, dapp_key::new(), key_tuple, value_tuple, ${resourceAccountArg}, OFFCHAIN, ctx);
810
+ ${fns.set_record}<DappKey>(${authArg(projectName)}${dappHubArg(
811
+ projectName,
812
+ isGlobal
813
+ )}${storageParam}, key_tuple, field_names, value_tuple, OFFCHAIN${ctxArg});
650
814
  }`
651
815
  : '';
652
816
 
@@ -704,7 +868,7 @@ function generateTableFunctions(
704
868
  }
705
869
 
706
870
  public fun decode(data: vector<u8>): ${toPascalCase(componentName)} {
707
- let mut bsc_type = sui::bcs::new(data);
871
+ let mut bcs_type = sui::bcs::new(data);
708
872
  ${valueNames
709
873
  .map((n) => {
710
874
  const fieldType = fields[n];
@@ -712,12 +876,12 @@ function generateTableFunctions(
712
876
  const enumType = isEnum ? enumTypes.find((e) => e.type === fieldType) : null;
713
877
  return `let ${n} = ${
714
878
  fieldType === 'string' || fieldType === 'String'
715
- ? `string(sui::bcs::peel_vec_u8(&mut bsc_type))`
879
+ ? `string(sui::bcs::peel_vec_u8(&mut bcs_type))`
716
880
  : fieldType === 'vector<String>'
717
- ? `dubhe::bcs::peel_vec_string(&mut bsc_type)`
881
+ ? `dubhe::bcs::peel_vec_string(&mut bcs_type)`
718
882
  : isEnum
719
- ? `${projectName}::${enumType?.module}::decode(&mut bsc_type)`
720
- : `sui::bcs::peel_${getBcsType(fieldType)}(&mut bsc_type)`
883
+ ? `${projectName}::${enumType?.module}::decode(&mut bcs_type)`
884
+ : `sui::bcs::peel_${getBcsType(fieldType)}(&mut bcs_type)`
721
885
  };`;
722
886
  })
723
887
  .join('\n ')}
@@ -770,15 +934,12 @@ function generateTableFunctions(
770
934
  }
771
935
 
772
936
  function toPascalCase(str: string): string {
773
- // Split the underscore-separated string into words
774
937
  return str
775
938
  .split('_')
776
939
  .map((word, _index) => {
777
- // If the word is a pure number, return it as is
778
940
  if (/^\d+$/.test(word)) {
779
941
  return word;
780
942
  }
781
- // Otherwise, capitalize the first letter and lowercase the rest
782
943
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
783
944
  })
784
945
  .join('');
@@ -819,3 +980,494 @@ function getBcsType(type: string): string {
819
980
  return type;
820
981
  }
821
982
  }
983
+
984
+ // ─── Annotation-based code extensions ────────────────────────────────────────
985
+
986
+ /**
987
+ * Generate additional Move functions for a resource based on its annotations.
988
+ * Returns a string of extra function bodies to inject into the module, or '' if none.
989
+ */
990
+ function generateAnnotationExtensions(
991
+ config: DubheConfig,
992
+ componentName: string,
993
+ comp: Component
994
+ ): string {
995
+ const parts: string[] = [];
996
+ const projectName = config.name;
997
+ const mod = getDappModuleName(projectName);
998
+ const auth = authArg(projectName);
999
+
1000
+ const fields = comp.fields;
1001
+ const keys = comp.keys ?? [];
1002
+ const valueFields = Object.entries(fields).filter(([n]) => !keys.includes(n));
1003
+ const valueNames = valueFields.map(([n]) => n);
1004
+ const keyParams = keys.length > 0 ? keys.map((k) => `${k}: ${fields[k]}`).join(', ') : '';
1005
+
1006
+ // ── fungible: true ────────────────────────────────────────────────────────
1007
+ if (comp.fungible && valueNames.length === 1) {
1008
+ const [_vName, vType] = valueFields[0];
1009
+ parts.push(`
1010
+ // ─── fungible add / sub ─────────────────────────────────────────────
1011
+ #[error]
1012
+ const EInsufficientAmount: vector<u8> = b"Insufficient amount";
1013
+
1014
+ public(package) fun add(user_storage: &mut UserStorage, amount: ${vType}, ctx: &mut TxContext) {
1015
+ let current = if (has(user_storage)) { get(user_storage) } else { 0 };
1016
+ set(user_storage, current + amount, ctx);
1017
+ }
1018
+
1019
+ public(package) fun sub(user_storage: &mut UserStorage, amount: ${vType}, ctx: &mut TxContext) {
1020
+ let current = get(user_storage);
1021
+ assert!(current >= amount, EInsufficientAmount);
1022
+ set(user_storage, current - amount, ctx);
1023
+ }`);
1024
+ }
1025
+
1026
+ // ── keys: auto-generate mint for any keyed resource ─────────────────────
1027
+ // Developer provides all key values — ID strategy is intentionally caller-decided.
1028
+ if (keys.length > 0 && !comp.offchain && !comp.global) {
1029
+ const allKeyParams = keys.map((k) => `${k}: ${fields[k]}`).join(', ');
1030
+ const valueParams = valueNames.map((n) => `${n}: ${fields[n]}`).join(', ');
1031
+ const allParams = [allKeyParams, valueParams].filter(Boolean).join(', ');
1032
+ const keyArgs = keys.join(', ');
1033
+ const setArgs = [...keys, ...valueNames].join(', ');
1034
+
1035
+ parts.push(`
1036
+ // ─── keys: mint (developer provides keys; framework ensures no duplicate) ─
1037
+ // Choosing the ID strategy (fresh address, counter, coordinate pack, etc.)
1038
+ // is intentionally left to the caller.
1039
+ public(package) fun mint(
1040
+ user_storage: &mut UserStorage,
1041
+ ${allParams},
1042
+ ctx: &mut TxContext,
1043
+ ) {
1044
+ ensure_has_not(user_storage, ${keyArgs});
1045
+ set(user_storage, ${setArgs}, ctx);
1046
+ }`);
1047
+ }
1048
+
1049
+ // ── reactive: true ────────────────────────────────────────────────────────
1050
+ if (comp.reactive) {
1051
+ const keyTupleCode =
1052
+ keys.length > 0
1053
+ ? `let mut key_tuple = vector::empty();\n key_tuple.push_back(TABLE_NAME);\n ${keys
1054
+ .map((k) => `key_tuple.push_back(sui::bcs::to_bytes(&${k}));`)
1055
+ .join('\n ')}`
1056
+ : `let mut key_tuple = vector::empty();\n key_tuple.push_back(TABLE_NAME);`;
1057
+
1058
+ // Full reactive set
1059
+ if (valueNames.length > 1) {
1060
+ const params = valueNames.map((n) => `${n}: ${fields[n]}`).join(', ');
1061
+ parts.push(`
1062
+ // ─── reactive: cross-user write variants ───────────────────────────
1063
+ // Package-level helpers: add pause checks and access control in your system
1064
+ // functions before calling these.
1065
+ public(package) fun set_reactive<PermType>(
1066
+ permit: &ScenePermit<PermType>,
1067
+ from: &mut UserStorage,
1068
+ target: &mut UserStorage,
1069
+ ${keyParams ? keyParams + ', ' : ''}${params},
1070
+ ctx: &mut TxContext,
1071
+ ) {
1072
+ ${keyTupleCode}
1073
+ let field_names = vector[${valueNames.map((n) => `b"${n}"`).join(', ')}];
1074
+ let value_tuple = encode(${valueNames.join(', ')});
1075
+ ${mod}::set_record_reactive<DappKey, PermType>(${auth}permit, from, target, key_tuple, field_names, value_tuple, ctx);
1076
+ }`);
1077
+ }
1078
+
1079
+ // Per-field reactive setters
1080
+ for (const [fName, fType] of valueFields) {
1081
+ const encodeExpr =
1082
+ fType === 'string' || fType === 'String'
1083
+ ? `sui::bcs::to_bytes(&std::ascii::into_bytes(${fName}))`
1084
+ : `sui::bcs::to_bytes(&${fName})`;
1085
+ parts.push(`
1086
+ public(package) fun set_${fName}_reactive<PermType>(
1087
+ permit: &ScenePermit<PermType>,
1088
+ from: &mut UserStorage,
1089
+ target: &mut UserStorage,
1090
+ ${keyParams ? keyParams + ', ' : ''}${fName}: ${
1091
+ fType === 'string' || fType === 'String' ? 'String' : fType
1092
+ },
1093
+ ctx: &mut TxContext,
1094
+ ) {
1095
+ ${keyTupleCode}
1096
+ let value = ${encodeExpr};
1097
+ ${mod}::set_field_reactive<DappKey, PermType>(${auth}permit, from, target, key_tuple, b"${fName}", value, ctx);
1098
+ }`);
1099
+ }
1100
+ }
1101
+
1102
+ // ── transferable: true — generate transfer functions ─────────────────────
1103
+ if (comp.transferable) {
1104
+ const objects = config.objects ?? {};
1105
+ const scenes = config.scenes ?? {};
1106
+ const isFungible = !!comp.fungible;
1107
+ // Trigger for any non-fungible keyed resource
1108
+ const isKeyed = !isFungible && keys.length > 0;
1109
+ const idField = isKeyed ? keys[0] : null;
1110
+
1111
+ for (const [objKey, objCfg] of Object.entries(objects)) {
1112
+ if (!(objCfg.accepts ?? []).includes(componentName)) continue;
1113
+ // Use fully-qualified ObjectStorage<Marker> type
1114
+ const objMarker = toPascalCase(objKey);
1115
+ const ObjStruct = `dubhe::dapp_service::ObjectStorage<${projectName}::${objKey}::${objMarker}>`;
1116
+ const objMod = objKey;
1117
+
1118
+ if (isFungible && valueNames.length === 1) {
1119
+ const [, vType] = valueFields[0];
1120
+ parts.push(`
1121
+ // ─── transferable: User ↔ ${objMarker}Storage (fungible) ─────────────
1122
+ public(package) fun transfer_user_to_${objKey}(
1123
+ user: &mut UserStorage,
1124
+ target: &mut ${ObjStruct},
1125
+ amount: ${vType},
1126
+ ctx: &mut TxContext,
1127
+ ) {
1128
+ sub(user, amount, ctx);
1129
+ ${projectName}::${objMod}::add_${componentName}(target, amount);
1130
+ }
1131
+
1132
+ public(package) fun transfer_${objKey}_to_user(
1133
+ source: &mut ${ObjStruct},
1134
+ user: &mut UserStorage,
1135
+ amount: ${vType},
1136
+ ctx: &mut TxContext,
1137
+ ) {
1138
+ ${projectName}::${objMod}::sub_${componentName}(source, amount);
1139
+ add(user, amount, ctx);
1140
+ }`);
1141
+ } else if (isKeyed && idField) {
1142
+ if (valueNames.length === 1) {
1143
+ const [, svType] = valueFields[0];
1144
+ const encodeRaw =
1145
+ svType === 'string' || svType === 'String'
1146
+ ? `to_bytes(&std::ascii::into_bytes(get(user, ${idField})))`
1147
+ : `to_bytes(&get(user, ${idField}))`;
1148
+ const enumTypes = Object.entries(config.enums ?? {}).map(([n]) => ({
1149
+ type: toPascalCase(n),
1150
+ module: n
1151
+ }));
1152
+ const peelExpr = buildParseExpr(projectName, svType, 'bcs', enumTypes);
1153
+ parts.push(`
1154
+ // ─── transferable: User ↔ ${objMarker}Storage (keyed) ──────────────
1155
+ public(package) fun transfer_user_to_${objKey}(
1156
+ user: &mut UserStorage,
1157
+ target: &mut ${ObjStruct},
1158
+ ${idField}: u64,
1159
+ ctx: &TxContext,
1160
+ ) {
1161
+ ensure_has(user, ${idField});
1162
+ // Guard before any mutation: abort if target already holds this item.
1163
+ dubhe::error::item_already_owned(!${projectName}::${objMod}::has_${componentName}(target, ${idField}));
1164
+ let raw = ${encodeRaw};
1165
+ delete(user, ${idField}, ctx);
1166
+ ${projectName}::${objMod}::set_${componentName}_data(target, ${idField}, raw);
1167
+ }
1168
+
1169
+ public(package) fun transfer_${objKey}_to_user(
1170
+ source: &mut ${ObjStruct},
1171
+ user: &mut UserStorage,
1172
+ ${idField}: u64,
1173
+ ctx: &mut TxContext,
1174
+ ) {
1175
+ // Guard before any mutation: abort if user already owns this item.
1176
+ ensure_has_not(user, ${idField});
1177
+ let raw = ${projectName}::${objMod}::remove_${componentName}_data(source, ${idField});
1178
+ let mut bcs = sui::bcs::new(raw);
1179
+ let value = ${peelExpr};
1180
+ set(user, ${idField}, value, ctx);
1181
+ }`);
1182
+ } else {
1183
+ parts.push(`
1184
+ // ─── transferable: User ↔ ${objMarker}Storage (keyed, multi-field) ──
1185
+ public(package) fun transfer_user_to_${objKey}(
1186
+ user: &mut UserStorage,
1187
+ target: &mut ${ObjStruct},
1188
+ ${idField}: u64,
1189
+ ctx: &TxContext,
1190
+ ) {
1191
+ ensure_has(user, ${idField});
1192
+ // Guard before any mutation: abort if target already holds this item.
1193
+ dubhe::error::item_already_owned(!${projectName}::${objMod}::has_${componentName}(target, ${idField}));
1194
+ let data = encode_struct(get_struct(user, ${idField}));
1195
+ delete(user, ${idField}, ctx);
1196
+ let raw: vector<u8> = sui::bcs::to_bytes(&data);
1197
+ ${projectName}::${objMod}::set_${componentName}_data(target, ${idField}, raw);
1198
+ }
1199
+
1200
+ public(package) fun transfer_${objKey}_to_user(
1201
+ source: &mut ${ObjStruct},
1202
+ user: &mut UserStorage,
1203
+ ${idField}: u64,
1204
+ ctx: &mut TxContext,
1205
+ ) {
1206
+ // Guard before any mutation: abort if user already owns this item.
1207
+ ensure_has_not(user, ${idField});
1208
+ let raw = ${projectName}::${objMod}::remove_${componentName}_data(source, ${idField});
1209
+ let decoded = decode(raw);
1210
+ set_struct(user, ${idField}, decoded, ctx);
1211
+ }`);
1212
+ }
1213
+ }
1214
+ }
1215
+
1216
+ for (const [sceneKey, sceneCfg] of Object.entries(scenes)) {
1217
+ if (!(sceneCfg.accepts ?? []).includes(componentName)) continue;
1218
+ // Use fully-qualified SceneStorage<Marker> type
1219
+ const sceneMarker = toPascalCase(sceneKey);
1220
+ const SceneStruct = `dubhe::dapp_service::SceneStorage<${projectName}::${sceneKey}::${sceneMarker}>`;
1221
+ const sceneMod = sceneKey;
1222
+ const scenePermitType =
1223
+ sceneCfg.authorization.kind === 'permit'
1224
+ ? `dubhe::dapp_service::ScenePermit<${projectName}::${
1225
+ sceneCfg.authorization.permit
1226
+ }::${toPascalCase(sceneCfg.authorization.permit)}>`
1227
+ : '';
1228
+ const scenePermitParam =
1229
+ sceneCfg.authorization.kind === 'permit' ? ` permit: &${scenePermitType},\n` : '';
1230
+ const scenePermitArg = sceneCfg.authorization.kind === 'permit' ? 'permit, ' : '';
1231
+ // Permit-authorized scene writes identify the caller via their UserStorage,
1232
+ // which for these transfers is the `user` side of the transfer itself.
1233
+ const sceneUserCall = sceneCfg.authorization.kind === 'permit' ? 'user, ' : '';
1234
+ const sceneCtxArg = sceneCfg.authorization.kind === 'permit' ? ', ctx' : '';
1235
+
1236
+ if (isFungible && valueNames.length === 1) {
1237
+ const [, vType] = valueFields[0];
1238
+ parts.push(`
1239
+ // ─── transferable: User ↔ ${sceneMarker}Storage (fungible) ──────────
1240
+ public(package) fun transfer_user_to_${sceneKey}(
1241
+ ${scenePermitParam} user: &mut UserStorage,
1242
+ target: &mut ${SceneStruct},
1243
+ amount: ${vType},
1244
+ ctx: &mut TxContext,
1245
+ ) {
1246
+ sub(user, amount, ctx);
1247
+ ${projectName}::${sceneMod}::add_${componentName}(${scenePermitArg}target, ${sceneUserCall}amount${sceneCtxArg});
1248
+ }
1249
+
1250
+ // ★ No expiry check on withdraw direction — prevents asset lock-in expired scenes.
1251
+ public(package) fun transfer_${sceneKey}_to_user(
1252
+ ${scenePermitParam} source: &mut ${SceneStruct},
1253
+ user: &mut UserStorage,
1254
+ amount: ${vType},
1255
+ ctx: &mut TxContext,
1256
+ ) {
1257
+ ${projectName}::${sceneMod}::sub_${componentName}(${scenePermitArg}source, ${sceneUserCall}amount${sceneCtxArg});
1258
+ add(user, amount, ctx);
1259
+ }`);
1260
+ } else if (isKeyed && idField) {
1261
+ if (valueNames.length === 1) {
1262
+ const [, svType] = valueFields[0];
1263
+ const encodeRaw =
1264
+ svType === 'string' || svType === 'String'
1265
+ ? `to_bytes(&std::ascii::into_bytes(get(user, ${idField})))`
1266
+ : `to_bytes(&get(user, ${idField}))`;
1267
+ const enumTypes = Object.entries(config.enums ?? {}).map(([n]) => ({
1268
+ type: toPascalCase(n),
1269
+ module: n
1270
+ }));
1271
+ const peelExpr = buildParseExpr(projectName, svType, 'bcs', enumTypes);
1272
+ parts.push(`
1273
+ // ─── transferable: User ↔ ${sceneMarker}Storage (keyed) ─────────────
1274
+ public(package) fun transfer_user_to_${sceneKey}(
1275
+ ${scenePermitParam} user: &mut UserStorage,
1276
+ target: &mut ${SceneStruct},
1277
+ ${idField}: u64,
1278
+ ctx: &TxContext,
1279
+ ) {
1280
+ ensure_has(user, ${idField});
1281
+ // Guard before any mutation: abort if target already holds this item.
1282
+ dubhe::error::item_already_owned(!${projectName}::${sceneMod}::has_${componentName}(target, ${idField}));
1283
+ let raw = ${encodeRaw};
1284
+ delete(user, ${idField}, ctx);
1285
+ ${projectName}::${sceneMod}::set_${componentName}_data(${scenePermitArg}target, ${sceneUserCall}${idField}, raw${sceneCtxArg});
1286
+ }
1287
+
1288
+ public(package) fun transfer_${sceneKey}_to_user(
1289
+ ${scenePermitParam} source: &mut ${SceneStruct},
1290
+ user: &mut UserStorage,
1291
+ ${idField}: u64,
1292
+ ctx: &mut TxContext,
1293
+ ) {
1294
+ // Guard before any mutation: abort if user already owns this item.
1295
+ ensure_has_not(user, ${idField});
1296
+ let raw = ${projectName}::${sceneMod}::remove_${componentName}_data(${scenePermitArg}source, ${sceneUserCall}${idField}${sceneCtxArg});
1297
+ let mut bcs = sui::bcs::new(raw);
1298
+ let value = ${peelExpr};
1299
+ set(user, ${idField}, value, ctx);
1300
+ }`);
1301
+ } else {
1302
+ parts.push(`
1303
+ // ─── transferable: User ↔ ${sceneMarker}Storage (keyed, multi-field) ─
1304
+ public(package) fun transfer_user_to_${sceneKey}(
1305
+ ${scenePermitParam} user: &mut UserStorage,
1306
+ target: &mut ${SceneStruct},
1307
+ ${idField}: u64,
1308
+ ctx: &TxContext,
1309
+ ) {
1310
+ ensure_has(user, ${idField});
1311
+ // Guard before any mutation: abort if target already holds this item.
1312
+ dubhe::error::item_already_owned(!${projectName}::${sceneMod}::has_${componentName}(target, ${idField}));
1313
+ let data = encode_struct(get_struct(user, ${idField}));
1314
+ delete(user, ${idField}, ctx);
1315
+ let raw: vector<u8> = sui::bcs::to_bytes(&data);
1316
+ ${projectName}::${sceneMod}::set_${componentName}_data(${scenePermitArg}target, ${sceneUserCall}${idField}, raw${sceneCtxArg});
1317
+ }
1318
+
1319
+ public(package) fun transfer_${sceneKey}_to_user(
1320
+ ${scenePermitParam} source: &mut ${SceneStruct},
1321
+ user: &mut UserStorage,
1322
+ ${idField}: u64,
1323
+ ctx: &mut TxContext,
1324
+ ) {
1325
+ // Guard before any mutation: abort if user already owns this item.
1326
+ ensure_has_not(user, ${idField});
1327
+ let raw = ${projectName}::${sceneMod}::remove_${componentName}_data(${scenePermitArg}source, ${sceneUserCall}${idField}${sceneCtxArg});
1328
+ let decoded = decode(raw);
1329
+ set_struct(user, ${idField}, decoded, ctx);
1330
+ }`);
1331
+ }
1332
+ }
1333
+ }
1334
+ }
1335
+
1336
+ // ── listable: true ────────────────────────────────────────────────────────
1337
+ if (comp.listable) {
1338
+ const isFungible = !!comp.fungible;
1339
+ // Any keyed non-fungible listable resource gets record-based marketplace helpers.
1340
+ const isKeyed = !isFungible && keys.length > 0;
1341
+ const isUnique = isKeyed;
1342
+ const idField = isUnique ? keys[0] : null;
1343
+ const tableNameExpr = `b"${componentName}"`;
1344
+
1345
+ if (isFungible && valueNames.length === 1) {
1346
+ // Fungible listing helpers — package-visible so developers must expose them
1347
+ // through their own system functions where they can add custom guards
1348
+ // (pause checks, access control, etc.).
1349
+ parts.push(`
1350
+ // ─── listable: market protocol (fungible) ──────────────────────────
1351
+ // Package-level helpers: call these from your system functions.
1352
+ // Add pause checks, access control, and custom logic there.
1353
+ public(package) fun list<CoinType>(
1354
+ user_storage: &mut UserStorage,
1355
+ amount: u64,
1356
+ price: u64,
1357
+ listed_until: std::option::Option<u64>,
1358
+ ctx: &mut TxContext,
1359
+ ) {
1360
+ dubhe::dapp_system::take_fungible_record<DappKey, CoinType>(
1361
+ dapp_key::new(),
1362
+ user_storage,
1363
+ ${tableNameExpr},
1364
+ { let mut k = vector::empty(); k.push_back(TABLE_NAME); k },
1365
+ b"${valueNames[0]}",
1366
+ amount,
1367
+ price,
1368
+ listed_until,
1369
+ ctx,
1370
+ );
1371
+ }
1372
+
1373
+ public(package) fun buy<CoinType>(
1374
+ dh: &dubhe::dapp_service::DappHub,
1375
+ dapp_storage: &mut DappStorage,
1376
+ listing: dubhe::dapp_service::Listing<CoinType>,
1377
+ user_storage: &mut UserStorage,
1378
+ payment: sui::coin::Coin<CoinType>,
1379
+ ctx: &mut TxContext,
1380
+ ): sui::coin::Coin<CoinType> {
1381
+ dubhe::dapp_system::buy_fungible_record<DappKey, CoinType>(
1382
+ dapp_key::new(), dh, dapp_storage, listing, user_storage, payment, ctx
1383
+ )
1384
+ }
1385
+
1386
+ public(package) fun cancel_listing<CoinType>(
1387
+ listing: dubhe::dapp_service::Listing<CoinType>,
1388
+ user_storage: &mut UserStorage,
1389
+ ctx: &TxContext,
1390
+ ) {
1391
+ dubhe::dapp_system::cancel_fungible_listing<DappKey, CoinType>(
1392
+ dapp_key::new(), listing, user_storage, ctx
1393
+ );
1394
+ }
1395
+
1396
+ public(package) fun expire_listing<CoinType>(
1397
+ listing: dubhe::dapp_service::Listing<CoinType>,
1398
+ user_storage: &mut UserStorage,
1399
+ ctx: &TxContext,
1400
+ ) {
1401
+ dubhe::dapp_system::expire_fungible_listing<DappKey, CoinType>(
1402
+ dapp_key::new(), listing, user_storage, ctx
1403
+ );
1404
+ }`);
1405
+ } else if (isUnique && idField) {
1406
+ // Build key params and record_key bytes using ALL keys (not just idField/keys[0])
1407
+ const listKeyParams = keys.map((k) => `${k}: ${fields[k] as string}`).join(',\n ');
1408
+ const listRecordKeyLines = keys
1409
+ .map((k) => `record_key.push_back(sui::bcs::to_bytes(&${k}));`)
1410
+ .join('\n ');
1411
+ parts.push(`
1412
+ // ─── listable: market protocol (keyed) ──────────────────────────────
1413
+ // Package-level helpers: call these from your system functions.
1414
+ // Add pause checks, access control, and custom logic there.
1415
+ public(package) fun list<CoinType>(
1416
+ user_storage: &mut UserStorage,
1417
+ ${listKeyParams},
1418
+ price: u64,
1419
+ listed_until: std::option::Option<u64>,
1420
+ ctx: &mut TxContext,
1421
+ ) {
1422
+ let mut record_key = vector::empty();
1423
+ record_key.push_back(TABLE_NAME);
1424
+ ${listRecordKeyLines}
1425
+ dubhe::dapp_system::take_record<DappKey, CoinType>(
1426
+ dapp_key::new(),
1427
+ user_storage,
1428
+ ${tableNameExpr},
1429
+ record_key,
1430
+ vector[${valueNames.map((n) => `b"${n}"`).join(', ')}],
1431
+ price,
1432
+ listed_until,
1433
+ ctx,
1434
+ );
1435
+ }
1436
+
1437
+ public(package) fun buy<CoinType>(
1438
+ dh: &dubhe::dapp_service::DappHub,
1439
+ dapp_storage: &mut DappStorage,
1440
+ listing: dubhe::dapp_service::Listing<CoinType>,
1441
+ user_storage: &mut UserStorage,
1442
+ payment: sui::coin::Coin<CoinType>,
1443
+ ctx: &mut TxContext,
1444
+ ): sui::coin::Coin<CoinType> {
1445
+ dubhe::dapp_system::buy_record<DappKey, CoinType>(
1446
+ dapp_key::new(), dh, dapp_storage, listing, user_storage, payment, ctx
1447
+ )
1448
+ }
1449
+
1450
+ public(package) fun cancel_listing<CoinType>(
1451
+ listing: dubhe::dapp_service::Listing<CoinType>,
1452
+ user_storage: &mut UserStorage,
1453
+ ctx: &TxContext,
1454
+ ) {
1455
+ dubhe::dapp_system::restore_record<DappKey, CoinType>(
1456
+ dapp_key::new(), listing, user_storage, ctx
1457
+ );
1458
+ }
1459
+
1460
+ public(package) fun expire_listing<CoinType>(
1461
+ listing: dubhe::dapp_service::Listing<CoinType>,
1462
+ user_storage: &mut UserStorage,
1463
+ ctx: &TxContext,
1464
+ ) {
1465
+ dubhe::dapp_system::expire_listing<DappKey, CoinType>(
1466
+ dapp_key::new(), listing, user_storage, ctx
1467
+ );
1468
+ }`);
1469
+ }
1470
+ }
1471
+
1472
+ return parts.join('\n');
1473
+ }