@0xobelisk/sui-common 1.2.0-pre.98 → 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.
- package/LICENSE +92 -0
- package/README.md +670 -1
- package/dist/index.d.ts +61 -27
- package/dist/index.js +1078 -461
- package/dist/index.js.map +1 -1
- package/package.json +15 -17
- package/src/codegen/debug.ts +0 -4
- package/src/codegen/types/index.ts +46 -10
- package/src/codegen/utils/config.ts +1 -1
- package/src/codegen/utils/format.ts +0 -6
- package/src/codegen/utils/formatAndWrite.ts +10 -31
- package/src/codegen/utils/generateLock.ts +122 -0
- package/src/codegen/utils/index.ts +4 -2
- package/src/codegen/utils/renderMove/{schemaGen.ts → codegen.ts} +40 -28
- package/src/codegen/utils/renderMove/common.ts +0 -65
- package/src/codegen/utils/renderMove/dapp.ts +2 -14
- package/src/codegen/utils/renderMove/generateDappKey.ts +33 -18
- package/src/codegen/utils/renderMove/generateError.ts +32 -15
- package/src/codegen/utils/renderMove/generateGenesis.ts +55 -22
- package/src/codegen/utils/renderMove/generateInitTest.ts +26 -14
- package/src/codegen/utils/renderMove/generateObjects.ts +377 -0
- package/src/codegen/utils/renderMove/generatePermits.ts +151 -0
- package/src/codegen/utils/renderMove/generateResources.ts +894 -242
- package/src/codegen/utils/renderMove/generateScenes.ts +467 -0
- package/src/codegen/utils/renderMove/generateScript.ts +18 -13
- package/src/codegen/utils/renderMove/generateSystem.ts +0 -2
- package/src/codegen/utils/renderMove/generateUserStorageInit.ts +37 -0
- package/src/codegen/utils/validateConfig.ts +237 -0
- package/src/index.ts +0 -1
- package/src/modules.d.ts +0 -10
- package/src/codegen/modules.d.ts +0 -1
- package/src/codegen/utils/posixPath.ts +0 -8
- package/src/codegen/utils/renderMove/generateComponents.ts +0 -802
- package/src/codegen/utils/renderMove/generateDefaultSchema.ts +0 -216
- package/src/codegen/utils/renderMove/generateEvent.ts +0 -99
- package/src/codegen/utils/renderMove/generateSchema.ts +0 -287
- package/src/codegen/utils/renderMove/generateSchemaHub.ts +0 -60
- package/src/parseData/index.ts +0 -1
- 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
|
|
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(
|
|
125
|
+
public fun has(${storageParam}: &${storageType}): bool {
|
|
54
126
|
let mut key_tuple = vector::empty();
|
|
55
127
|
key_tuple.push_back(TABLE_NAME);
|
|
56
|
-
${
|
|
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
|
|
131
|
+
public fun ensure_has(${storageParam}: &${storageType}) {
|
|
62
132
|
let mut key_tuple = vector::empty();
|
|
63
133
|
key_tuple.push_back(TABLE_NAME);
|
|
64
|
-
${
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
93
|
-
use
|
|
94
|
-
use
|
|
95
|
-
|
|
96
|
-
|
|
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(
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
${
|
|
118
|
-
|
|
119
|
-
|
|
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 =
|
|
242
|
+
const global = resource.global || false;
|
|
149
243
|
const type: ComponentType = offchain ? 'Offchain' : 'Onchain';
|
|
150
244
|
|
|
151
|
-
|
|
152
|
-
const
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
?
|
|
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
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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(
|
|
388
|
-
|
|
389
|
-
}${
|
|
562
|
+
? ` public fun has(${storageParam}: &${storageType}${
|
|
563
|
+
keyParams ? ', ' : ''
|
|
564
|
+
}${keyParams}): bool {
|
|
390
565
|
${keyTupleCode}
|
|
391
|
-
${
|
|
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(
|
|
397
|
-
resourceAccountParam || keyParams ? ', ' : ''
|
|
398
|
-
}${resourceAccountParam}${paramSeparator}${keyParams}) {
|
|
569
|
+
public fun ensure_has(${storageParam}: &${storageType}${keyParams ? ', ' : ''}${keyParams}) {
|
|
399
570
|
${keyTupleCode}
|
|
400
|
-
${
|
|
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(
|
|
406
|
-
|
|
407
|
-
|
|
574
|
+
public fun ensure_has_not(${storageParam}: &${storageType}${
|
|
575
|
+
keyParams ? ', ' : ''
|
|
576
|
+
}${keyParams}) {
|
|
408
577
|
${keyTupleCode}
|
|
409
|
-
${
|
|
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(
|
|
419
|
-
|
|
420
|
-
}${
|
|
587
|
+
? ` public(package) fun delete(${storageParam}: &mut ${storageType}${
|
|
588
|
+
keyParams ? ', ' : ''
|
|
589
|
+
}${keyParams}${isGlobal ? '' : ', ctx: &TxContext'}) {
|
|
421
590
|
${keyTupleCode}
|
|
422
|
-
${
|
|
423
|
-
|
|
424
|
-
|
|
591
|
+
${fns.delete_record}<DappKey>(${authArg(
|
|
592
|
+
projectName
|
|
593
|
+
)}${storageParam}, key_tuple, ${fieldNamesLiteral}${isGlobal ? '' : ', ctx'});
|
|
425
594
|
}`
|
|
426
595
|
: '';
|
|
427
596
|
|
|
428
|
-
// Generate getter
|
|
429
|
-
//
|
|
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}(
|
|
440
|
-
|
|
441
|
-
}${
|
|
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
|
|
450
|
-
|
|
451
|
-
|
|
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}(
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
${
|
|
485
|
-
|
|
486
|
-
|
|
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(
|
|
495
|
-
|
|
496
|
-
|
|
657
|
+
? ` public(package) fun set(${dappHubParam(
|
|
658
|
+
projectName,
|
|
659
|
+
isGlobal
|
|
660
|
+
)}${storageParam}: &mut ${storageType}${keyParams ? ', ' : ''}${keyParams}${ctxParam}) {
|
|
497
661
|
${keyTupleCode}
|
|
498
|
-
let
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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(
|
|
506
|
-
|
|
507
|
-
}${
|
|
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
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
let mut
|
|
517
|
-
${
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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(
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
}
|
|
699
|
+
}${ctxParam}) {
|
|
540
700
|
${keyTupleCode}
|
|
701
|
+
let field_names = vector[b"${valueNames[0]}"];
|
|
541
702
|
let value_tuple = encode(value);
|
|
542
|
-
${
|
|
543
|
-
projectName
|
|
544
|
-
|
|
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(
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
}
|
|
715
|
+
}${ctxParam}) {
|
|
553
716
|
${keyTupleCode}
|
|
717
|
+
let field_names = vector[b"${valueNames[0]}"];
|
|
554
718
|
let value_tuple = encode(value);
|
|
555
|
-
${
|
|
556
|
-
projectName
|
|
557
|
-
|
|
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(
|
|
561
|
-
|
|
562
|
-
}${
|
|
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
|
-
|
|
573
|
-
|
|
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(
|
|
590
|
-
|
|
591
|
-
|
|
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(', ')}
|
|
746
|
+
.join(', ')}${ctxParam}) {
|
|
596
747
|
${keyTupleCode}
|
|
748
|
+
let field_names = ${fieldNamesVec};
|
|
597
749
|
let value_tuple = encode(${valueNames.join(', ')});
|
|
598
|
-
${
|
|
599
|
-
|
|
600
|
-
|
|
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(
|
|
603
|
-
|
|
604
|
-
|
|
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(', ')}
|
|
762
|
+
.join(', ')}${ctxParam}) {
|
|
609
763
|
${keyTupleCode}
|
|
764
|
+
let field_names = ${fieldNamesVec};
|
|
610
765
|
let value_tuple = encode(${valueNames.join(', ')});
|
|
611
|
-
${
|
|
612
|
-
|
|
613
|
-
|
|
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(
|
|
620
|
-
|
|
621
|
-
}${
|
|
775
|
+
? ` public fun get_struct(${storageParam}: &${storageType}${
|
|
776
|
+
keyParams ? ', ' : ''
|
|
777
|
+
}${keyParams}): ${toPascalCase(componentName)} {
|
|
622
778
|
${keyTupleCode}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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(
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
${
|
|
637
|
-
projectName
|
|
638
|
-
|
|
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(
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
${
|
|
648
|
-
projectName
|
|
649
|
-
|
|
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
|
|
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
|
|
879
|
+
? `string(sui::bcs::peel_vec_u8(&mut bcs_type))`
|
|
716
880
|
: fieldType === 'vector<String>'
|
|
717
|
-
? `dubhe::bcs::peel_vec_string(&mut
|
|
881
|
+
? `dubhe::bcs::peel_vec_string(&mut bcs_type)`
|
|
718
882
|
: isEnum
|
|
719
|
-
? `${projectName}::${enumType?.module}::decode(&mut
|
|
720
|
-
: `sui::bcs::peel_${getBcsType(fieldType)}(&mut
|
|
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
|
+
}
|