@0xobelisk/sui-cli 1.2.0-pre.117 → 1.2.0-pre.119
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/dist/dubhe.js +120 -111
- package/dist/dubhe.js.map +1 -1
- package/package.json +3 -3
- package/src/commands/build.ts +7 -1
- package/src/commands/convertJson.ts +28 -14
- package/src/commands/generate.ts +13 -4
- package/src/commands/publish.ts +23 -1
- package/src/commands/test.ts +6 -0
- package/src/commands/upgrade.ts +25 -3
- package/src/utils/publishHandler.ts +6 -2
- package/src/utils/storeConfig.ts +20 -0
- package/src/utils/upgradeHandler.ts +36 -21
- package/src/utils/utils.ts +200 -31
package/src/utils/utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fsAsync from 'fs/promises';
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
3
3
|
import { dirname, join as pathJoin } from 'path';
|
|
4
|
+
import * as readline from 'readline';
|
|
4
5
|
import { SUI_PRIVATE_KEY_PREFIX } from '@mysten/sui/cryptography';
|
|
5
6
|
import { FsIibError } from './errors';
|
|
6
7
|
import * as fs from 'fs';
|
|
@@ -21,6 +22,13 @@ export type DeploymentJsonType = {
|
|
|
21
22
|
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet';
|
|
22
23
|
startCheckpoint: string;
|
|
23
24
|
packageId: string;
|
|
25
|
+
/**
|
|
26
|
+
* The original (first-published) package ID of this dapp.
|
|
27
|
+
* Derived from type_name::with_defining_ids<DappKey>() in Move, so it is stable
|
|
28
|
+
* across upgrades and is the canonical identifier used in dapp_key and indexer filtering.
|
|
29
|
+
* Set once at publish time and never changed during upgrades.
|
|
30
|
+
*/
|
|
31
|
+
originalPackageId: string;
|
|
24
32
|
/** Object ID of the Dubhe framework's DappHub shared object. */
|
|
25
33
|
dappHubId: string;
|
|
26
34
|
/**
|
|
@@ -206,6 +214,7 @@ export async function saveContractData(
|
|
|
206
214
|
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
207
215
|
startCheckpoint: string,
|
|
208
216
|
packageId: string,
|
|
217
|
+
originalPackageId: string,
|
|
209
218
|
dappHubId: string,
|
|
210
219
|
upgradeCap: string,
|
|
211
220
|
version: number,
|
|
@@ -219,6 +228,7 @@ export async function saveContractData(
|
|
|
219
228
|
network,
|
|
220
229
|
startCheckpoint,
|
|
221
230
|
packageId,
|
|
231
|
+
originalPackageId,
|
|
222
232
|
dappHubId,
|
|
223
233
|
frameworkPackageId,
|
|
224
234
|
dappStorageId,
|
|
@@ -799,6 +809,11 @@ export function initializeDubhe({
|
|
|
799
809
|
}
|
|
800
810
|
|
|
801
811
|
export function generateConfigJson(config: DubheConfig): string {
|
|
812
|
+
const serializeFields = (fields: Record<string, unknown> = {}) =>
|
|
813
|
+
Object.entries(fields).map(([fieldName, fieldType]) => ({
|
|
814
|
+
[fieldName]: fieldType
|
|
815
|
+
}));
|
|
816
|
+
|
|
802
817
|
const resources = Object.entries(config.resources ?? {}).map(([name, resource]) => {
|
|
803
818
|
// Simple type shorthand (e.g., counter1: 'u32') – entity-keyed by account (entity_id: String).
|
|
804
819
|
if (typeof resource === 'string') {
|
|
@@ -857,25 +872,6 @@ export function generateConfigJson(config: DubheConfig): string {
|
|
|
857
872
|
};
|
|
858
873
|
});
|
|
859
874
|
|
|
860
|
-
// Auto-append Dubhe framework fee state resource (entity-keyed by account string).
|
|
861
|
-
if (!resources.some((resource) => 'dapp_fee_state' in resource)) {
|
|
862
|
-
resources.push({
|
|
863
|
-
dapp_fee_state: {
|
|
864
|
-
fields: [
|
|
865
|
-
{ entity_id: 'String' },
|
|
866
|
-
{ base_fee: 'u256' },
|
|
867
|
-
{ bytes_fee: 'u256' },
|
|
868
|
-
{ free_credit: 'u256' },
|
|
869
|
-
{ credit_pool: 'u256' },
|
|
870
|
-
{ total_settled: 'u256' },
|
|
871
|
-
{ suspended: 'bool' }
|
|
872
|
-
],
|
|
873
|
-
keys: ['entity_id'],
|
|
874
|
-
offchain: false
|
|
875
|
-
}
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
|
|
879
875
|
// handle enums
|
|
880
876
|
const enums = Object.entries(config.enums || {}).map(([name, enumFields]) => {
|
|
881
877
|
// Sort enum values by first letter
|
|
@@ -886,9 +882,34 @@ export function generateConfigJson(config: DubheConfig): string {
|
|
|
886
882
|
};
|
|
887
883
|
});
|
|
888
884
|
|
|
885
|
+
const objects = Object.entries(config.objects ?? {}).map(([name, object]) => ({
|
|
886
|
+
[name]: {
|
|
887
|
+
fields: serializeFields(object.fields),
|
|
888
|
+
accepts: object.accepts ?? [],
|
|
889
|
+
acceptsFrom: object.acceptsFrom ?? [],
|
|
890
|
+
adminOnly: object.adminOnly ?? false
|
|
891
|
+
}
|
|
892
|
+
}));
|
|
893
|
+
|
|
894
|
+
const scenes = Object.entries(config.scenes ?? {}).map(([name, scene]) => ({
|
|
895
|
+
[name]: {
|
|
896
|
+
fields: serializeFields(scene.fields),
|
|
897
|
+
authorization: scene.authorization,
|
|
898
|
+
accepts: scene.accepts ?? [],
|
|
899
|
+
acceptsFrom: scene.acceptsFrom ?? []
|
|
900
|
+
}
|
|
901
|
+
}));
|
|
902
|
+
|
|
903
|
+
const permits = Object.entries(config.permits ?? {}).map(([name, permit]) => ({
|
|
904
|
+
[name]: permit ?? {}
|
|
905
|
+
}));
|
|
906
|
+
|
|
889
907
|
return JSON.stringify(
|
|
890
908
|
{
|
|
891
909
|
resources,
|
|
910
|
+
objects,
|
|
911
|
+
scenes,
|
|
912
|
+
permits,
|
|
892
913
|
enums
|
|
893
914
|
},
|
|
894
915
|
null,
|
|
@@ -968,15 +989,21 @@ export function updateGenesisUpgradeFunction(path: string, tables: string[]) {
|
|
|
968
989
|
}
|
|
969
990
|
|
|
970
991
|
/**
|
|
971
|
-
* Appends a `migrate_to_vN` entry function to the package's migrate.move
|
|
992
|
+
* Appends a `migrate_to_vN` entry function to the package's migrate.move and
|
|
993
|
+
* bumps `ON_CHAIN_VERSION` to `newVersion`.
|
|
994
|
+
*
|
|
995
|
+
* Called by upgradeHandler when new resources are detected (pendingMigration.length > 0).
|
|
996
|
+
* The generated function:
|
|
997
|
+
* 1. Reads the new package ID via `dapp_key::package_id()` — available on the new package.
|
|
998
|
+
* 2. Reads the target version via `migrate::on_chain_version()` — equals newVersion after
|
|
999
|
+
* this function bumps the constant.
|
|
1000
|
+
* 3. Calls `dapp_system::upgrade_dapp` to register the new package ID and bump
|
|
1001
|
+
* `DappStorage.version`.
|
|
1002
|
+
* 4. Calls `genesis::migrate` for any custom migration logic (extension point).
|
|
972
1003
|
*
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
* for any future resource-registration steps. The `new_package_id` and
|
|
977
|
-
* `new_version` arguments are kept in the signature to match the on-chain
|
|
978
|
-
* call emitted by upgradeHandler even though dapp_system::upgrade_dapp is
|
|
979
|
-
* public(package) in dubhe and cannot be called from external packages.
|
|
1004
|
+
* `upgrade_dapp` accepts the new package's DappKey because its check was changed to compare
|
|
1005
|
+
* the caller's package ID against the registered list OR the incoming new_package_id, rather
|
|
1006
|
+
* than doing a full type-string comparison that would always fail after an upgrade.
|
|
980
1007
|
*/
|
|
981
1008
|
export function appendMigrateFunction(
|
|
982
1009
|
projectPath: string,
|
|
@@ -988,21 +1015,38 @@ export function appendMigrateFunction(
|
|
|
988
1015
|
throw new Error(`migrate.move not found at ${migratePath}`);
|
|
989
1016
|
}
|
|
990
1017
|
|
|
991
|
-
|
|
1018
|
+
let content = fs.readFileSync(migratePath, 'utf-8');
|
|
992
1019
|
|
|
993
|
-
// Idempotency: skip if the function already exists
|
|
1020
|
+
// Idempotency: skip entirely if the function already exists
|
|
994
1021
|
if (content.includes(`migrate_to_v${newVersion}`)) {
|
|
995
1022
|
return;
|
|
996
1023
|
}
|
|
997
1024
|
|
|
1025
|
+
// ── Step 1: bump ON_CHAIN_VERSION to newVersion ──────────────────────────────
|
|
1026
|
+
// Replace the first `ON_CHAIN_VERSION: u32 = <N>` constant in the file.
|
|
1027
|
+
// This ensures on_chain_version() returns the correct value when upgrade_dapp
|
|
1028
|
+
// reads it inside the generated migrate_to_vN function.
|
|
1029
|
+
content = content.replace(
|
|
1030
|
+
/const ON_CHAIN_VERSION:\s*u32\s*=\s*\d+\s*;/,
|
|
1031
|
+
`const ON_CHAIN_VERSION: u32 = ${newVersion};`
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
// ── Step 2: append migrate_to_vN ─────────────────────────────────────────────
|
|
1035
|
+
// new_package_id must be passed as a parameter because type_name::get<T>() in
|
|
1036
|
+
// Sui Move always returns the ORIGINAL (genesis) package ID, not the upgraded one.
|
|
1037
|
+
// The TypeScript upgradeHandler supplies the actual new package ID after the upgrade
|
|
1038
|
+
// transaction completes and the on-chain package address is known.
|
|
998
1039
|
const migrateFunction = `
|
|
999
1040
|
public entry fun migrate_to_v${newVersion}(
|
|
1000
1041
|
dapp_hub: &mut dubhe::dapp_service::DappHub,
|
|
1001
1042
|
dapp_storage: &mut dubhe::dapp_service::DappStorage,
|
|
1002
|
-
|
|
1003
|
-
_new_version: u32,
|
|
1043
|
+
new_package_id: address,
|
|
1004
1044
|
ctx: &mut TxContext
|
|
1005
1045
|
) {
|
|
1046
|
+
let new_version = ${packageName}::migrate::on_chain_version();
|
|
1047
|
+
dubhe::dapp_system::upgrade_dapp<${packageName}::dapp_key::DappKey>(
|
|
1048
|
+
dapp_hub, dapp_storage, new_package_id, new_version, ctx
|
|
1049
|
+
);
|
|
1006
1050
|
${packageName}::genesis::migrate(dapp_hub, dapp_storage, ctx);
|
|
1007
1051
|
}
|
|
1008
1052
|
`;
|
|
@@ -1017,3 +1061,128 @@ export function appendMigrateFunction(
|
|
|
1017
1061
|
content.slice(0, closingBraceIdx) + migrateFunction + content.slice(closingBraceIdx);
|
|
1018
1062
|
fs.writeFileSync(migratePath, updated, 'utf-8');
|
|
1019
1063
|
}
|
|
1064
|
+
|
|
1065
|
+
// ---------------------------------------------------------------------------
|
|
1066
|
+
// Guard lint
|
|
1067
|
+
// ---------------------------------------------------------------------------
|
|
1068
|
+
|
|
1069
|
+
export type MissingGuardResult = {
|
|
1070
|
+
/** Relative path to the Move source file (for display). */
|
|
1071
|
+
file: string;
|
|
1072
|
+
/** Name of the entry function missing the guard. */
|
|
1073
|
+
fn: string;
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Scans every `*.move` file under `<projectPath>/sources/systems/` and returns
|
|
1078
|
+
* the list of `public entry fun` declarations that:
|
|
1079
|
+
* 1. Accept a `DappStorage` parameter (so a version check is applicable), AND
|
|
1080
|
+
* 2. Do NOT call `ensure_latest_version` anywhere in their body.
|
|
1081
|
+
*
|
|
1082
|
+
* The implementation uses brace-balancing to extract each function body rather
|
|
1083
|
+
* than a full AST parse, which is sufficient for this structural check.
|
|
1084
|
+
*/
|
|
1085
|
+
export function lintSystemGuards(projectPath: string): MissingGuardResult[] {
|
|
1086
|
+
const systemsDir = pathJoin(projectPath, 'sources', 'systems');
|
|
1087
|
+
if (!fs.existsSync(systemsDir)) return [];
|
|
1088
|
+
|
|
1089
|
+
const results: MissingGuardResult[] = [];
|
|
1090
|
+
const files = fs.readdirSync(systemsDir).filter((f) => f.endsWith('.move'));
|
|
1091
|
+
|
|
1092
|
+
for (const file of files) {
|
|
1093
|
+
const fullPath = pathJoin(systemsDir, file);
|
|
1094
|
+
const src = fs.readFileSync(fullPath, 'utf-8');
|
|
1095
|
+
|
|
1096
|
+
// Find every `public entry fun <name>` position.
|
|
1097
|
+
const entryFunRe = /public\s+entry\s+fun\s+(\w+)\s*\(/g;
|
|
1098
|
+
let match: RegExpExecArray | null;
|
|
1099
|
+
|
|
1100
|
+
while ((match = entryFunRe.exec(src)) !== null) {
|
|
1101
|
+
const fnName = match[1];
|
|
1102
|
+
const parenStart = match.index + match[0].length - 1; // position of '('
|
|
1103
|
+
|
|
1104
|
+
// Extract the parameter list (between the outermost parentheses).
|
|
1105
|
+
let depth = 0;
|
|
1106
|
+
let parenEnd = parenStart;
|
|
1107
|
+
for (let i = parenStart; i < src.length; i++) {
|
|
1108
|
+
if (src[i] === '(') depth++;
|
|
1109
|
+
else if (src[i] === ')') {
|
|
1110
|
+
depth--;
|
|
1111
|
+
if (depth === 0) {
|
|
1112
|
+
parenEnd = i;
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const paramList = src.slice(parenStart + 1, parenEnd);
|
|
1118
|
+
|
|
1119
|
+
// Only flag functions that receive a DappStorage parameter.
|
|
1120
|
+
if (!/DappStorage/.test(paramList)) continue;
|
|
1121
|
+
|
|
1122
|
+
// Extract the function body (between the outermost braces after the params).
|
|
1123
|
+
const braceStart = src.indexOf('{', parenEnd);
|
|
1124
|
+
if (braceStart === -1) continue;
|
|
1125
|
+
|
|
1126
|
+
depth = 0;
|
|
1127
|
+
let braceEnd = braceStart;
|
|
1128
|
+
for (let i = braceStart; i < src.length; i++) {
|
|
1129
|
+
if (src[i] === '{') depth++;
|
|
1130
|
+
else if (src[i] === '}') {
|
|
1131
|
+
depth--;
|
|
1132
|
+
if (depth === 0) {
|
|
1133
|
+
braceEnd = i;
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const body = src.slice(braceStart, braceEnd + 1);
|
|
1139
|
+
|
|
1140
|
+
if (!/ensure_latest_version/.test(body)) {
|
|
1141
|
+
results.push({ file, fn: fnName });
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
return results;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Formats lint results as a human-readable warning block.
|
|
1151
|
+
* Returns an empty string when there are no issues.
|
|
1152
|
+
*/
|
|
1153
|
+
export function formatLintWarnings(results: MissingGuardResult[]): string {
|
|
1154
|
+
if (results.length === 0) return '';
|
|
1155
|
+
const lines: string[] = [
|
|
1156
|
+
chalk.yellow('⚠️ Missing ensure_latest_version in the following entry functions:'),
|
|
1157
|
+
chalk.yellow(' Old-package callers can still invoke these functions after an upgrade.'),
|
|
1158
|
+
''
|
|
1159
|
+
];
|
|
1160
|
+
for (const r of results) {
|
|
1161
|
+
lines.push(chalk.yellow(` • ${r.file} → ${r.fn}()`));
|
|
1162
|
+
}
|
|
1163
|
+
lines.push('');
|
|
1164
|
+
lines.push(
|
|
1165
|
+
chalk.yellow(
|
|
1166
|
+
' Fix: add dubhe::dapp_system::ensure_latest_version(dapp_storage); at the top of each function.'
|
|
1167
|
+
)
|
|
1168
|
+
);
|
|
1169
|
+
lines.push('');
|
|
1170
|
+
return lines.join('\n');
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Prompts the user for a yes/no confirmation on stdout/stdin.
|
|
1175
|
+
* Resolves `true` for "y/Y", `false` for everything else.
|
|
1176
|
+
*/
|
|
1177
|
+
export function confirm(question: string): Promise<boolean> {
|
|
1178
|
+
return new Promise((resolve) => {
|
|
1179
|
+
const rl = readline.createInterface({
|
|
1180
|
+
input: process.stdin,
|
|
1181
|
+
output: process.stdout
|
|
1182
|
+
});
|
|
1183
|
+
rl.question(chalk.yellow(`${question} [y/N] `), (answer: string) => {
|
|
1184
|
+
rl.close();
|
|
1185
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
1186
|
+
});
|
|
1187
|
+
});
|
|
1188
|
+
}
|