@0xobelisk/sui-client 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.
package/src/dubhe.ts CHANGED
@@ -16,7 +16,7 @@ import { SuiInteractor } from './libs/suiInteractor';
16
16
  import { MapObjectStruct, MoveStructType, MoveEnumType } from './types';
17
17
  import { SuiContractFactory } from './libs/suiContractFactory';
18
18
  import { SuiMoveMoudleFuncType } from './libs/suiContractFactory/types';
19
- import { getDefaultURL, NetworkConfig } from './libs/suiInteractor';
19
+ import { getDefaultConfig, NetworkConfig } from './libs/suiInteractor';
20
20
  import {
21
21
  ContractQuery,
22
22
  ContractTx,
@@ -27,10 +27,12 @@ import {
27
27
  DubheParams,
28
28
  SuiTxArg,
29
29
  SuiObjectArg,
30
- SuiVecTxArg
30
+ SuiVecTxArg,
31
+ NetworkType
31
32
  } from './types';
32
33
  import {
33
34
  BasicBcsTypes,
35
+ normalizeDappKey,
34
36
  normalizeHexAddress,
35
37
  normalizePackageId,
36
38
  numberToAddressHex
@@ -112,6 +114,9 @@ function createTx(
112
114
  );
113
115
  }
114
116
 
117
+ /** Sui's well-known shared Clock object ID, available on all networks. */
118
+ const SUI_CLOCK_OBJECT_ID = '0x0000000000000000000000000000000000000000000000000000000000000006';
119
+
115
120
  /**
116
121
  * @class Dubhe
117
122
  * @description This class is used to aggregate the tools that used to interact with SUI network.
@@ -123,8 +128,13 @@ export class Dubhe {
123
128
  public dubheChannelClient: DubheChannelClient;
124
129
 
125
130
  public packageId: string | undefined;
131
+ /** Fully-qualified Move type string for this DApp's DappKey, e.g. `<64-hex>::dapp_key::DappKey`. */
132
+ public dappKey: string | undefined;
126
133
  public metadata: SuiMoveNormalizedModules | undefined;
127
134
  public projectName: string | undefined;
135
+ public frameworkPackageId: string | undefined;
136
+ public dappStorageId: string | undefined;
137
+ public dappHubId: string | undefined;
128
138
 
129
139
  readonly #query: MapMoudleFuncQuery = {};
130
140
  readonly #tx: MapMoudleFuncTx = {};
@@ -143,6 +153,7 @@ export class Dubhe {
143
153
  * @param packageId
144
154
  * @param metadata
145
155
  * @param channelUrl, the base URL for Dubhe Channel API, optional
156
+ * @param frameworkPackageId, the published package ID of the dubhe framework, required for proxy operations
146
157
  */
147
158
  constructor({
148
159
  mnemonics,
@@ -150,12 +161,16 @@ export class Dubhe {
150
161
  networkType,
151
162
  fullnodeUrls,
152
163
  packageId,
164
+ dappKey: dappKeyParam,
153
165
  metadata,
154
- channelUrl
166
+ channelUrl,
167
+ frameworkPackageId,
168
+ dappStorageId,
169
+ dappHubId
155
170
  }: DubheParams = {}) {
156
171
  networkType = networkType ?? 'mainnet';
157
172
 
158
- const defaultParams = getDefaultURL(networkType);
173
+ const defaultParams = getDefaultConfig(networkType);
159
174
 
160
175
  // Init the account manager
161
176
  this.accountManager = new SuiAccountManager({ mnemonics, secretKey });
@@ -169,6 +184,19 @@ export class Dubhe {
169
184
  });
170
185
 
171
186
  this.packageId = packageId ? normalizePackageId(packageId) : undefined;
187
+ // Prefer the explicitly passed dappKey (stable across upgrades); fall back to
188
+ // computing it from packageId (only correct before the first upgrade).
189
+ this.dappKey = dappKeyParam;
190
+ // Prefer the explicitly provided frameworkPackageId; fall back to the
191
+ // well-known constant for the current network (defined for testnet/mainnet).
192
+ const networkDefault = defaultParams.frameworkPackageId;
193
+ this.frameworkPackageId = frameworkPackageId
194
+ ? normalizePackageId(frameworkPackageId)
195
+ : networkDefault
196
+ ? normalizePackageId(networkDefault)
197
+ : undefined;
198
+ this.dappStorageId = dappStorageId;
199
+ this.dappHubId = dappHubId;
172
200
  if (metadata !== undefined) {
173
201
  this.metadata = metadata as SuiMoveNormalizedModules;
174
202
 
@@ -1397,6 +1425,28 @@ export class Dubhe {
1397
1425
  return this.contractFactory.packageId;
1398
1426
  }
1399
1427
 
1428
+ /**
1429
+ * Return the fully-qualified Move type string for this DApp's `DappKey`.
1430
+ *
1431
+ * The format matches what `std::type_name::get<DappKey>()` returns on-chain:
1432
+ * `<64-char-zero-padded-address>::dapp_key::DappKey`
1433
+ *
1434
+ * Use this whenever you need to pass a type argument or build a `target`
1435
+ * string that references the DApp's DappKey type.
1436
+ *
1437
+ * @example
1438
+ * ```typescript
1439
+ * tx.moveCall({
1440
+ * target: `${frameworkPackageId}::dapp_system::settle_writes`,
1441
+ * typeArguments: [contract.getDappKey()],
1442
+ * arguments: [...],
1443
+ * });
1444
+ * ```
1445
+ */
1446
+ getDappKey() {
1447
+ return this.dappKey;
1448
+ }
1449
+
1400
1450
  getMetadata() {
1401
1451
  return this.contractFactory.metadata;
1402
1452
  }
@@ -1406,7 +1456,7 @@ export class Dubhe {
1406
1456
  }
1407
1457
 
1408
1458
  getNetworkConfig(): NetworkConfig {
1409
- return getDefaultURL(this.getNetwork());
1459
+ return getDefaultConfig(this.getNetwork());
1410
1460
  }
1411
1461
 
1412
1462
  getTxExplorerUrl(txHash: string) {
@@ -1441,7 +1491,7 @@ export class Dubhe {
1441
1491
 
1442
1492
  if (networkChanged || fullnodeUrlsChanged) {
1443
1493
  const newNetworkType = config.networkType ?? this.suiInteractor.network;
1444
- const defaultParams = getDefaultURL(newNetworkType);
1494
+ const defaultParams = getDefaultConfig(newNetworkType);
1445
1495
  const newFullnodeUrls = config.fullnodeUrls || [defaultParams.fullNode];
1446
1496
 
1447
1497
  this.suiInteractor = new SuiInteractor(newFullnodeUrls, newNetworkType);
@@ -1458,6 +1508,11 @@ export class Dubhe {
1458
1508
  const packageIdChanged = config.packageId !== undefined && config.packageId !== this.packageId;
1459
1509
  const metadataChanged = config.metadata !== undefined && config.metadata !== this.metadata;
1460
1510
 
1511
+ // dappKey is independent of packageId — update whenever explicitly provided.
1512
+ if (config.dappKey !== undefined) {
1513
+ this.dappKey = config.dappKey;
1514
+ }
1515
+
1461
1516
  if (packageIdChanged || metadataChanged) {
1462
1517
  // Update packageId
1463
1518
  if (config.packageId !== undefined) {
@@ -1703,15 +1758,12 @@ export class Dubhe {
1703
1758
  }) {
1704
1759
  packageId = packageId || this.packageId;
1705
1760
  account = account || this.getAddress();
1706
- // Remove 0x prefix if present
1761
+ // Remove 0x prefix from account if present (required by the channel server)
1707
1762
  if (account && account.startsWith('0x')) {
1708
1763
  account = account.slice(2);
1709
1764
  }
1710
- if (packageId && packageId.startsWith('0x')) {
1711
- packageId = packageId.slice(2);
1712
- }
1713
1765
  const payload = {
1714
- dapp_key: `${packageId}::dapp_key::DappKey`,
1766
+ dapp_key: normalizeDappKey(packageId ?? ''),
1715
1767
  account: account,
1716
1768
  table: table,
1717
1769
  key: key
@@ -1759,7 +1811,7 @@ export class Dubhe {
1759
1811
  */
1760
1812
  async subscribeChannelTable(
1761
1813
  {
1762
- packageId,
1814
+ packageId: _packageId,
1763
1815
  account,
1764
1816
  table,
1765
1817
  key
@@ -1776,20 +1828,13 @@ export class Dubhe {
1776
1828
  onClose?: () => void;
1777
1829
  }
1778
1830
  ) {
1779
- packageId = packageId || this.packageId;
1780
-
1781
- // Remove 0x prefix if present (required by the channel server)
1782
- if (account !== undefined) {
1783
- if (account.startsWith('0x')) {
1784
- account = account.slice(2);
1785
- }
1786
- }
1787
- if (packageId && packageId.startsWith('0x')) {
1788
- packageId = packageId.slice(2);
1831
+ // Remove 0x prefix from account if present (required by the channel server)
1832
+ if (account !== undefined && account.startsWith('0x')) {
1833
+ account = account.slice(2);
1789
1834
  }
1790
1835
 
1791
1836
  const payload: any = {
1792
- dapp_key: `${packageId}::dapp_key::DappKey`
1837
+ dapp_key: this.dappKey
1793
1838
  };
1794
1839
 
1795
1840
  // Only include account if specified
@@ -2023,6 +2068,996 @@ export class Dubhe {
2023
2068
  return numberToAddressHex(x);
2024
2069
  }
2025
2070
 
2071
+ // ─── UserStorage helpers ────────────────────────────────────────────────────
2072
+
2073
+ /**
2074
+ * Call the DApp's generated `user_storage_init::init_user_storage` entry function,
2075
+ * which creates and shares a `UserStorage` object for the signer.
2076
+ *
2077
+ * Each DApp exposes this entry point under its own package ID.
2078
+ * On Sui, the `UserStorage` object is shared immediately so all transactions
2079
+ * can reference it without knowing the object ID in advance.
2080
+ *
2081
+ * @param dappHubId Object ID of the DappHub shared object
2082
+ * @param dappStorageId Object ID of the DApp's DappStorage shared object
2083
+ */
2084
+ async initUserStorage({
2085
+ dappHubId,
2086
+ dappStorageId: dappStorageIdParam,
2087
+ derivePathParams,
2088
+ onSuccess,
2089
+ onError
2090
+ }: {
2091
+ dappHubId: string;
2092
+ dappStorageId?: string;
2093
+ derivePathParams?: DerivePathParams;
2094
+ onSuccess?: (result: SuiTransactionBlockResponse) => void | Promise<void>;
2095
+ onError?: (error: Error) => void | Promise<void>;
2096
+ }): Promise<SuiTransactionBlockResponse> {
2097
+ const storageId = dappStorageIdParam ?? this.dappStorageId;
2098
+ if (!storageId) {
2099
+ throw new Error(
2100
+ 'dappStorageId is required for initUserStorage. ' +
2101
+ 'Pass it directly or set it in the Dubhe constructor.'
2102
+ );
2103
+ }
2104
+ const pkg = this.packageId;
2105
+ if (!pkg) throw new Error('packageId is required for initUserStorage.');
2106
+ const tx = new Transaction();
2107
+ tx.moveCall({
2108
+ target: `${pkg}::user_storage_init::init_user_storage`,
2109
+ arguments: [tx.object(dappHubId), tx.object(storageId)]
2110
+ });
2111
+ return this.signAndSendTxn({
2112
+ tx,
2113
+ derivePathParams,
2114
+ onSuccess,
2115
+ onError
2116
+ }) as Promise<SuiTransactionBlockResponse>;
2117
+ }
2118
+
2119
+ /**
2120
+ * Activate a session key for the signer's `UserStorage`.
2121
+ *
2122
+ * The signer must be the `canonical_owner` of the `UserStorage` object.
2123
+ * Calls `<frameworkPackageId>::dapp_system::activate_session<DappKey>`.
2124
+ *
2125
+ * @param userStorageId Object ID of the UserStorage to activate a session for
2126
+ * @param sessionWallet Sui address of the session wallet
2127
+ * @param durationMs Session duration in milliseconds ([60_000, 604_800_000])
2128
+ * @param clockObjectId Object ID of the Sui Clock (default: 0x6)
2129
+ */
2130
+ async activateSession({
2131
+ userStorageId,
2132
+ sessionWallet,
2133
+ durationMs,
2134
+ dappHubId: dappHubIdParam,
2135
+ clockObjectId,
2136
+ derivePathParams,
2137
+ onSuccess,
2138
+ onError
2139
+ }: {
2140
+ userStorageId: string;
2141
+ sessionWallet: string;
2142
+ durationMs: number;
2143
+ dappHubId?: string;
2144
+ clockObjectId?: string;
2145
+ derivePathParams?: DerivePathParams;
2146
+ onSuccess?: (result: SuiTransactionBlockResponse) => void | Promise<void>;
2147
+ onError?: (error: Error) => void | Promise<void>;
2148
+ }): Promise<SuiTransactionBlockResponse> {
2149
+ const fwPkg = this.frameworkPackageId;
2150
+ if (!fwPkg) {
2151
+ throw new Error(
2152
+ 'frameworkPackageId is required for activateSession. ' +
2153
+ 'Set it in the Dubhe constructor ({ frameworkPackageId: "0x..." }).'
2154
+ );
2155
+ }
2156
+ const typeArg = this.dappKey;
2157
+
2158
+ if (!typeArg) {
2159
+ throw new Error(
2160
+ 'dappKey is required for activateSession. ' +
2161
+ 'Set it in the Dubhe constructor ({ packageId: "0x..." }).'
2162
+ );
2163
+ }
2164
+
2165
+ const hubId = dappHubIdParam ?? this.dappHubId;
2166
+ if (!hubId) {
2167
+ throw new Error(
2168
+ 'dappHubId is required for activateSession. ' +
2169
+ 'Pass it directly or set it in the Dubhe constructor ({ dappHubId: "0x..." }).'
2170
+ );
2171
+ }
2172
+
2173
+ const clockId = clockObjectId ?? SUI_CLOCK_OBJECT_ID;
2174
+ const tx = new Transaction();
2175
+ tx.moveCall({
2176
+ target: `${fwPkg}::dapp_system::activate_session`,
2177
+ typeArguments: [typeArg],
2178
+ arguments: [
2179
+ tx.object(hubId),
2180
+ tx.object(userStorageId),
2181
+ tx.pure.address(sessionWallet),
2182
+ tx.pure.u64(durationMs),
2183
+ tx.object(clockId)
2184
+ ]
2185
+ });
2186
+ return this.signAndSendTxn({
2187
+ tx,
2188
+ derivePathParams,
2189
+ onSuccess,
2190
+ onError
2191
+ }) as Promise<SuiTransactionBlockResponse>;
2192
+ }
2193
+
2194
+ /**
2195
+ * Deactivate the current session key on a `UserStorage`.
2196
+ *
2197
+ * Allowed callers:
2198
+ * - The `canonical_owner` (revoke at any time).
2199
+ * - The session key itself (voluntary sign-out).
2200
+ * - Anyone, once the session has expired.
2201
+ *
2202
+ * Calls `<frameworkPackageId>::dapp_system::deactivate_session<DappKey>`.
2203
+ *
2204
+ * @param userStorageId Object ID of the UserStorage to deactivate the session on
2205
+ */
2206
+ async deactivateSession({
2207
+ userStorageId,
2208
+ dappHubId: dappHubIdParam,
2209
+ derivePathParams,
2210
+ onSuccess,
2211
+ onError
2212
+ }: {
2213
+ userStorageId: string;
2214
+ dappHubId?: string;
2215
+ derivePathParams?: DerivePathParams;
2216
+ onSuccess?: (result: SuiTransactionBlockResponse) => void | Promise<void>;
2217
+ onError?: (error: Error) => void | Promise<void>;
2218
+ }): Promise<SuiTransactionBlockResponse> {
2219
+ const fwPkg = this.frameworkPackageId;
2220
+ if (!fwPkg) {
2221
+ throw new Error(
2222
+ 'frameworkPackageId is required for deactivateSession. ' +
2223
+ 'Set it in the Dubhe constructor ({ frameworkPackageId: "0x..." }).'
2224
+ );
2225
+ }
2226
+ const typeArg = this.dappKey;
2227
+
2228
+ if (!typeArg) {
2229
+ throw new Error(
2230
+ 'dappKey is required for deactivateSession. ' +
2231
+ 'Set it in the Dubhe constructor ({ packageId: "0x..." }).'
2232
+ );
2233
+ }
2234
+
2235
+ const hubId = dappHubIdParam ?? this.dappHubId;
2236
+ if (!hubId) {
2237
+ throw new Error(
2238
+ 'dappHubId is required for deactivateSession. ' +
2239
+ 'Pass it directly or set it in the Dubhe constructor ({ dappHubId: "0x..." }).'
2240
+ );
2241
+ }
2242
+
2243
+ const tx = new Transaction();
2244
+ tx.moveCall({
2245
+ target: `${fwPkg}::dapp_system::deactivate_session`,
2246
+ typeArguments: [typeArg],
2247
+ arguments: [tx.object(hubId), tx.object(userStorageId)]
2248
+ });
2249
+ return this.signAndSendTxn({
2250
+ tx,
2251
+ derivePathParams,
2252
+ onSuccess,
2253
+ onError
2254
+ }) as Promise<SuiTransactionBlockResponse>;
2255
+ }
2256
+
2257
+ /**
2258
+ * Look up the `UserStorage` object ID for the given address in this DApp.
2259
+ *
2260
+ * Scans the transaction history for `user_storage_init::init_user_storage`
2261
+ * calls from the given address and extracts the created `UserStorage` object
2262
+ * ID from the transaction's object changes.
2263
+ *
2264
+ * Returns the object ID on success, or `null` when:
2265
+ * - `packageId` or `frameworkPackageId` is not set.
2266
+ * - The user has not yet called `initUserStorage`.
2267
+ *
2268
+ * @param userAddress Sui address to look up (with or without 0x prefix).
2269
+ */
2270
+ async getUserStorageId(userAddress: string): Promise<string | null> {
2271
+ const pkg = this.packageId;
2272
+ const fwPkg = this.frameworkPackageId;
2273
+ if (!pkg || !fwPkg) return null;
2274
+
2275
+ // Normalize to the 0x-prefixed form used in on-chain JSON.
2276
+ const normalizedUser = userAddress.startsWith('0x') ? userAddress : `0x${userAddress}`;
2277
+ // objectType includes the full framework package address prefix.
2278
+ const expectedType = `${fwPkg}::dapp_service::UserStorage`;
2279
+
2280
+ let cursor: string | null | undefined = null;
2281
+ let hasNextPage = true;
2282
+ while (hasNextPage) {
2283
+ const result = await this.suiInteractor.currentClient.queryTransactionBlocks({
2284
+ filter: {
2285
+ MoveFunction: {
2286
+ package: pkg,
2287
+ module: 'user_storage_init',
2288
+ function: 'init_user_storage'
2289
+ }
2290
+ },
2291
+ options: { showObjectChanges: true },
2292
+ ...(cursor !== null && cursor !== undefined ? { cursor } : {}),
2293
+ limit: 50
2294
+ });
2295
+
2296
+ for (const tx of result.data) {
2297
+ if (!tx.objectChanges) continue;
2298
+ // Each SuiObjectChangeCreated carries a `sender` field that equals the
2299
+ // transaction sender, which is the canonical_owner of the UserStorage.
2300
+ const created = (tx.objectChanges as any[]).find(
2301
+ (c: any) =>
2302
+ c.type === 'created' &&
2303
+ typeof c.objectType === 'string' &&
2304
+ c.objectType === expectedType &&
2305
+ c.sender === normalizedUser
2306
+ );
2307
+ if (created?.objectId) return created.objectId as string;
2308
+ }
2309
+
2310
+ hasNextPage = result.hasNextPage;
2311
+ cursor = result.nextCursor ?? null;
2312
+ }
2313
+
2314
+ return null;
2315
+ }
2316
+
2317
+ /**
2318
+ * Fetch and return the on-chain fields of a `UserStorage` shared object.
2319
+ *
2320
+ * The returned object mirrors Move's `UserStorage` struct plus two
2321
+ * convenience fields computed from the raw counters:
2322
+ * - `unsettled_count` = write_count − settled_count
2323
+ * - `unsettled_bytes` = write_bytes − settled_bytes
2324
+ *
2325
+ * @param userStorageId Object ID of the UserStorage to inspect.
2326
+ */
2327
+ async getUserStorageFields(userStorageId: string): Promise<{
2328
+ objectId: string;
2329
+ dapp_key: string;
2330
+ canonical_owner: string;
2331
+ session_key: string;
2332
+ session_expires_at: bigint;
2333
+ write_count: bigint;
2334
+ settled_count: bigint;
2335
+ write_bytes: bigint;
2336
+ settled_bytes: bigint;
2337
+ unsettled_count: bigint;
2338
+ unsettled_bytes: bigint;
2339
+ }> {
2340
+ const obj = await this.suiInteractor.getObject(userStorageId);
2341
+ const fields = (obj?.content as any)?.fields ?? {};
2342
+ const writeCount = BigInt(fields.write_count ?? 0);
2343
+ const settledCount = BigInt(fields.settled_count ?? 0);
2344
+ const writeBytes = BigInt(fields.write_bytes ?? 0);
2345
+ const settledBytes = BigInt(fields.settled_bytes ?? 0);
2346
+ return {
2347
+ objectId: userStorageId,
2348
+ dapp_key: fields.dapp_key ?? '',
2349
+ canonical_owner: fields.canonical_owner ?? '',
2350
+ session_key: fields.session_key ?? '',
2351
+ session_expires_at: BigInt(fields.session_expires_at ?? 0),
2352
+ write_count: writeCount,
2353
+ settled_count: settledCount,
2354
+ write_bytes: writeBytes,
2355
+ settled_bytes: settledBytes,
2356
+ unsettled_count: writeCount - settledCount,
2357
+ unsettled_bytes: writeBytes - settledBytes
2358
+ };
2359
+ }
2360
+
2361
+ /**
2362
+ * Fetch and return the on-chain fields of a `DappStorage` shared object.
2363
+ *
2364
+ * Covers the full metadata + fee/credit subset of the Move struct.
2365
+ *
2366
+ * @param dappStorageId Object ID of the DappStorage to inspect.
2367
+ */
2368
+ /**
2369
+ * Read the framework-level fields from the DappHub shared object.
2370
+ *
2371
+ * Key fields:
2372
+ * - `treasury` — address that directly receives framework fee income
2373
+ * - `pending_treasury` — queued treasury address during two-step rotation
2374
+ * - `framework_admin` — address that can update framework configuration
2375
+ * - `version` — current framework version
2376
+ */
2377
+ async getDappHubFields(dappHubId: string): Promise<{
2378
+ objectId: string;
2379
+ version: number;
2380
+ framework_admin: string;
2381
+ treasury: string;
2382
+ pending_treasury: string;
2383
+ }> {
2384
+ const obj = await this.suiInteractor.getObject(dappHubId);
2385
+ const fields = (obj?.content as any)?.fields ?? {};
2386
+ // fee_config is a nested object; its fields contain treasury etc.
2387
+ const feeConfig = fields.fee_config?.fields ?? fields.fee_config ?? {};
2388
+ const adminConfig = fields.admin_config?.fields ?? fields.admin_config ?? {};
2389
+ return {
2390
+ objectId: dappHubId,
2391
+ version: Number(fields.version ?? 0),
2392
+ framework_admin: adminConfig.admin ?? fields.framework_admin ?? '',
2393
+ treasury: feeConfig.treasury ?? fields.treasury ?? '',
2394
+ pending_treasury: feeConfig.pending_treasury ?? fields.pending_treasury ?? ''
2395
+ };
2396
+ }
2397
+
2398
+ async getDappStorageFields(dappStorageId: string): Promise<{
2399
+ objectId: string;
2400
+ dapp_key: string;
2401
+ name: string;
2402
+ description: string;
2403
+ website_url: string;
2404
+ admin: string;
2405
+ version: number;
2406
+ paused: boolean;
2407
+ free_credit: bigint;
2408
+ free_credit_expires_at: bigint;
2409
+ credit_pool: bigint;
2410
+ min_credit_to_unsuspend: bigint;
2411
+ suspended: boolean;
2412
+ total_settled: bigint;
2413
+ base_fee_per_write: bigint;
2414
+ bytes_fee_per_byte: bigint;
2415
+ /**
2416
+ * Settlement mode: 0 = DAPP_SUBSIDIZES (operator pays from credit_pool),
2417
+ * 1 = USER_PAYS (user provides Coin via settle_writes_user_pays).
2418
+ * Only `settle_writes` (no-coin variant) is safe to auto-call in mode 0.
2419
+ */
2420
+ settlement_mode: number;
2421
+ /**
2422
+ * BPS (0-10000) of each write-fee payment that flows into the DApp's
2423
+ * revenue pool. The remainder goes to the framework treasury.
2424
+ * Set exclusively by the framework admin via set_dapp_write_fee_share.
2425
+ * Default: 0 (100% to framework treasury).
2426
+ */
2427
+ write_fee_dapp_share_bps: number;
2428
+ }> {
2429
+ const obj = await this.suiInteractor.getObject(dappStorageId);
2430
+ const fields = (obj?.content as any)?.fields ?? {};
2431
+ return {
2432
+ objectId: dappStorageId,
2433
+ dapp_key: fields.dapp_key ?? '',
2434
+ name: fields.name ?? '',
2435
+ description: fields.description ?? '',
2436
+ website_url: fields.website_url ?? '',
2437
+ admin: fields.admin ?? '',
2438
+ version: Number(fields.version ?? 0),
2439
+ paused: Boolean(fields.paused),
2440
+ free_credit: BigInt(fields.free_credit ?? 0),
2441
+ free_credit_expires_at: BigInt(fields.free_credit_expires_at ?? 0),
2442
+ credit_pool: BigInt(fields.credit_pool ?? 0),
2443
+ min_credit_to_unsuspend: BigInt(fields.min_credit_to_unsuspend ?? 0),
2444
+ suspended: Boolean(fields.suspended),
2445
+ total_settled: BigInt(fields.total_settled ?? 0),
2446
+ base_fee_per_write: BigInt(fields.base_fee_per_write ?? 0),
2447
+ bytes_fee_per_byte: BigInt(fields.bytes_fee_per_byte ?? 0),
2448
+ settlement_mode: Number(fields.settlement_mode ?? 0),
2449
+ write_fee_dapp_share_bps: Number(fields.write_fee_dapp_share_bps ?? 0)
2450
+ };
2451
+ }
2452
+
2453
+ /**
2454
+ * Append a `dapp_system::settle_writes` moveCall to an existing PTB.
2455
+ *
2456
+ * Does NOT send the transaction — call this before your own moveCall(s) to
2457
+ * settle any accumulated write debt within the same PTB at no extra round-trip.
2458
+ *
2459
+ * The framework function never aborts due to insufficient credit (it silently
2460
+ * skips or partially settles), so prepending this to any PTB is always safe.
2461
+ *
2462
+ * @param tx The Transaction object to append the moveCall to.
2463
+ * @param dappHubId Object ID of the DappHub shared object.
2464
+ * @param dappStorageId Object ID of the DApp's DappStorage (falls back to constructor value).
2465
+ * @param userStorageId Object ID of the user's UserStorage shared object.
2466
+ */
2467
+ buildSettleWritesTx(
2468
+ tx: Transaction,
2469
+ {
2470
+ dappHubId,
2471
+ dappStorageId: dappStorageIdParam,
2472
+ userStorageId
2473
+ }: { dappHubId: string; dappStorageId?: string; userStorageId: string }
2474
+ ): void {
2475
+ const fwPkg = this.frameworkPackageId;
2476
+ if (!fwPkg) {
2477
+ throw new Error(
2478
+ 'frameworkPackageId is required for buildSettleWritesTx. ' +
2479
+ 'Set it in the Dubhe constructor ({ frameworkPackageId: "0x..." }).'
2480
+ );
2481
+ }
2482
+ const storageId = dappStorageIdParam ?? this.dappStorageId;
2483
+ if (!storageId) {
2484
+ throw new Error(
2485
+ 'dappStorageId is required for buildSettleWritesTx. ' +
2486
+ 'Pass it directly or set it in the Dubhe constructor.'
2487
+ );
2488
+ }
2489
+ const typeArg = this.dappKey;
2490
+ if (!typeArg) {
2491
+ throw new Error(
2492
+ 'dappKey is required for buildSettleWritesTx. ' +
2493
+ 'Set it in the Dubhe constructor ({ packageId: "0x..." }).'
2494
+ );
2495
+ }
2496
+ tx.moveCall({
2497
+ target: `${fwPkg}::dapp_system::settle_writes`,
2498
+ typeArguments: [typeArg],
2499
+ arguments: [tx.object(dappHubId), tx.object(storageId), tx.object(userStorageId)]
2500
+ });
2501
+ }
2502
+
2503
+ /**
2504
+ * Settle accumulated write debt for a user (standalone transaction).
2505
+ *
2506
+ * Builds a PTB containing a single `dapp_system::settle_writes` call and
2507
+ * sends it. For embedding settlement into an existing PTB, use
2508
+ * `buildSettleWritesTx` instead.
2509
+ *
2510
+ * This is safe to call at any time — the framework function never aborts
2511
+ * due to insufficient credit; it silently skips or partially settles.
2512
+ *
2513
+ * @param dappHubId Object ID of the DappHub shared object.
2514
+ * @param dappStorageId Object ID of the DApp's DappStorage shared object.
2515
+ * @param userStorageId Object ID of the user's UserStorage shared object.
2516
+ */
2517
+ async settleWrites({
2518
+ dappHubId,
2519
+ dappStorageId,
2520
+ userStorageId,
2521
+ derivePathParams,
2522
+ onSuccess,
2523
+ onError
2524
+ }: {
2525
+ dappHubId: string;
2526
+ dappStorageId?: string;
2527
+ userStorageId: string;
2528
+ derivePathParams?: DerivePathParams;
2529
+ onSuccess?: (result: SuiTransactionBlockResponse) => void | Promise<void>;
2530
+ onError?: (error: Error) => void | Promise<void>;
2531
+ }): Promise<SuiTransactionBlockResponse> {
2532
+ const tx = new Transaction();
2533
+ this.buildSettleWritesTx(tx, { dappHubId, dappStorageId, userStorageId });
2534
+ return this.signAndSendTxn({
2535
+ tx,
2536
+ derivePathParams,
2537
+ onSuccess,
2538
+ onError
2539
+ }) as Promise<SuiTransactionBlockResponse>;
2540
+ }
2541
+
2542
+ /**
2543
+ * Recharge a DApp's credit pool by paying with the framework's accepted coin type.
2544
+ *
2545
+ * Wraps `dapp_system::recharge_credit<DappKey, CoinType>`. Any account may call
2546
+ * this — no admin restriction. Payment is forwarded to the framework treasury and
2547
+ * the DApp's `credit_pool` is increased by the coin value.
2548
+ *
2549
+ * Only valid in DAPP_SUBSIDIZES settlement mode. Aborts with
2550
+ * `wrong_settlement_mode` if the DApp is in USER_PAYS mode.
2551
+ *
2552
+ * @param dappHubId Object ID of the DappHub shared object.
2553
+ * @param dappStorageId Object ID of the DApp's DappStorage shared object.
2554
+ * @param coinObjectId Object ID of the Coin to use as payment.
2555
+ * @param coinType Move type of the coin (default: "0x2::sui::SUI").
2556
+ */
2557
+ async rechargeCredit({
2558
+ dappHubId,
2559
+ dappStorageId: dappStorageIdParam,
2560
+ coinObjectId,
2561
+ coinType = '0x2::sui::SUI',
2562
+ derivePathParams,
2563
+ onSuccess,
2564
+ onError
2565
+ }: {
2566
+ dappHubId: string;
2567
+ dappStorageId?: string;
2568
+ coinObjectId: string;
2569
+ coinType?: string;
2570
+ derivePathParams?: DerivePathParams;
2571
+ onSuccess?: (result: SuiTransactionBlockResponse) => void | Promise<void>;
2572
+ onError?: (error: Error) => void | Promise<void>;
2573
+ }): Promise<SuiTransactionBlockResponse> {
2574
+ const fwPkg = this.frameworkPackageId;
2575
+ if (!fwPkg) {
2576
+ throw new Error(
2577
+ 'frameworkPackageId is required for rechargeCredit. ' +
2578
+ 'Set it in the Dubhe constructor ({ frameworkPackageId: "0x..." }).'
2579
+ );
2580
+ }
2581
+ const storageId = dappStorageIdParam ?? this.dappStorageId;
2582
+ if (!storageId) {
2583
+ throw new Error(
2584
+ 'dappStorageId is required for rechargeCredit. ' +
2585
+ 'Pass it directly or set it in the Dubhe constructor.'
2586
+ );
2587
+ }
2588
+ const typeArg = this.dappKey;
2589
+ if (!typeArg) {
2590
+ throw new Error(
2591
+ 'dappKey is required for rechargeCredit. ' +
2592
+ 'Set it in the Dubhe constructor ({ packageId: "0x..." }).'
2593
+ );
2594
+ }
2595
+ const tx = new Transaction();
2596
+ tx.moveCall({
2597
+ target: `${fwPkg}::dapp_system::recharge_credit`,
2598
+ typeArguments: [typeArg, coinType],
2599
+ arguments: [tx.object(dappHubId), tx.object(storageId), tx.object(coinObjectId)]
2600
+ });
2601
+ return this.signAndSendTxn({
2602
+ tx,
2603
+ derivePathParams,
2604
+ onSuccess,
2605
+ onError
2606
+ }) as Promise<SuiTransactionBlockResponse>;
2607
+ }
2608
+
2609
+ /**
2610
+ * Append a `dapp_system::settle_writes_user_pays` moveCall to an existing PTB.
2611
+ *
2612
+ * Splits `maxPaymentMist` MIST from `tx.gas` as the payment coin, then calls
2613
+ * `settle_writes_user_pays`. The framework returns any overpayment automatically,
2614
+ * so it is safe to over-estimate. This lets a session key pay for settlement
2615
+ * inline without requiring a separate owned Coin object.
2616
+ *
2617
+ * @param tx The Transaction object to append the moveCall to.
2618
+ * @param dappHubId Object ID of the DappHub shared object.
2619
+ * @param dappStorageId Object ID of the DApp's DappStorage (falls back to constructor value).
2620
+ * @param userStorageId Object ID of the user's UserStorage shared object.
2621
+ * @param maxPaymentMist Upper bound for the payment in MIST (default: 50_000_000 = 0.05 SUI).
2622
+ * @param coinType Move type of the payment coin (default: "0x2::sui::SUI").
2623
+ */
2624
+ buildSettleWritesUserPaysTx(
2625
+ tx: Transaction,
2626
+ {
2627
+ dappHubId,
2628
+ dappStorageId: dappStorageIdParam,
2629
+ userStorageId,
2630
+ maxPaymentMist = 50_000_000n,
2631
+ coinType = '0x2::sui::SUI'
2632
+ }: {
2633
+ dappHubId: string;
2634
+ dappStorageId?: string;
2635
+ userStorageId: string;
2636
+ maxPaymentMist?: bigint;
2637
+ coinType?: string;
2638
+ }
2639
+ ): void {
2640
+ const fwPkg = this.frameworkPackageId;
2641
+ if (!fwPkg) {
2642
+ throw new Error(
2643
+ 'frameworkPackageId is required for buildSettleWritesUserPaysTx. ' +
2644
+ 'Set it in the Dubhe constructor ({ frameworkPackageId: "0x..." }).'
2645
+ );
2646
+ }
2647
+ const storageId = dappStorageIdParam ?? this.dappStorageId;
2648
+ if (!storageId) {
2649
+ throw new Error(
2650
+ 'dappStorageId is required for buildSettleWritesUserPaysTx. ' +
2651
+ 'Pass it directly or set it in the Dubhe constructor.'
2652
+ );
2653
+ }
2654
+ const typeArg = this.dappKey;
2655
+ if (!typeArg) {
2656
+ throw new Error(
2657
+ 'dappKey is required for buildSettleWritesUserPaysTx. ' +
2658
+ 'Set it in the Dubhe constructor ({ packageId: "0x..." }).'
2659
+ );
2660
+ }
2661
+ // Split payment from the PTB's gas coin — no separate Coin object needed.
2662
+ const [paymentCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(maxPaymentMist)]);
2663
+ // settle_writes_user_pays returns the change Coin (overpayment refund).
2664
+ // Coin<T> has no `drop` ability, so the returned value MUST be consumed.
2665
+ // Merge it back into tx.gas — it belongs to whoever is paying (the signer).
2666
+ const changeCoin = tx.moveCall({
2667
+ target: `${fwPkg}::dapp_system::settle_writes_user_pays`,
2668
+ typeArguments: [typeArg, coinType],
2669
+ arguments: [tx.object(dappHubId), tx.object(storageId), tx.object(userStorageId), paymentCoin]
2670
+ });
2671
+ tx.mergeCoins(tx.gas, [changeCoin]);
2672
+ }
2673
+
2674
+ /**
2675
+ * Settle accumulated write debt where the user pays directly (USER_PAYS mode).
2676
+ *
2677
+ * Wraps `dapp_system::settle_writes_user_pays<DappKey, CoinType>`. The exact
2678
+ * cost is computed on-chain; any overpayment from the coin is returned to the
2679
+ * sender after settlement.
2680
+ *
2681
+ * Aborts if:
2682
+ * - The DApp is not in USER_PAYS mode (`wrong_settlement_mode`)
2683
+ * - The coin type does not match the accepted type (`wrong_payment_coin_type`)
2684
+ * - The coin value is less than the total cost (`insufficient_credit`)
2685
+ *
2686
+ * @param dappHubId Object ID of the DappHub shared object.
2687
+ * @param dappStorageId Object ID of the DApp's DappStorage shared object.
2688
+ * @param userStorageId Object ID of the user's UserStorage shared object.
2689
+ * @param coinObjectId Object ID of the Coin to use as payment.
2690
+ * @param coinType Move type of the coin (default: "0x2::sui::SUI").
2691
+ */
2692
+ async settleWritesUserPays({
2693
+ dappHubId,
2694
+ dappStorageId: dappStorageIdParam,
2695
+ userStorageId,
2696
+ coinObjectId,
2697
+ coinType = '0x2::sui::SUI',
2698
+ derivePathParams,
2699
+ onSuccess,
2700
+ onError
2701
+ }: {
2702
+ dappHubId: string;
2703
+ dappStorageId?: string;
2704
+ userStorageId: string;
2705
+ coinObjectId: string;
2706
+ coinType?: string;
2707
+ derivePathParams?: DerivePathParams;
2708
+ onSuccess?: (result: SuiTransactionBlockResponse) => void | Promise<void>;
2709
+ onError?: (error: Error) => void | Promise<void>;
2710
+ }): Promise<SuiTransactionBlockResponse> {
2711
+ const fwPkg = this.frameworkPackageId;
2712
+ if (!fwPkg) {
2713
+ throw new Error(
2714
+ 'frameworkPackageId is required for settleWritesUserPays. ' +
2715
+ 'Set it in the Dubhe constructor ({ frameworkPackageId: "0x..." }).'
2716
+ );
2717
+ }
2718
+ const storageId = dappStorageIdParam ?? this.dappStorageId;
2719
+ if (!storageId) {
2720
+ throw new Error(
2721
+ 'dappStorageId is required for settleWritesUserPays. ' +
2722
+ 'Pass it directly or set it in the Dubhe constructor.'
2723
+ );
2724
+ }
2725
+ const typeArg = this.dappKey;
2726
+ if (!typeArg) {
2727
+ throw new Error(
2728
+ 'dappKey is required for settleWritesUserPays. ' +
2729
+ 'Set it in the Dubhe constructor ({ packageId: "0x..." }).'
2730
+ );
2731
+ }
2732
+ const tx = new Transaction();
2733
+ tx.moveCall({
2734
+ target: `${fwPkg}::dapp_system::settle_writes_user_pays`,
2735
+ typeArguments: [typeArg, coinType],
2736
+ arguments: [
2737
+ tx.object(dappHubId),
2738
+ tx.object(storageId),
2739
+ tx.object(userStorageId),
2740
+ tx.object(coinObjectId)
2741
+ ]
2742
+ });
2743
+ return this.signAndSendTxn({
2744
+ tx,
2745
+ derivePathParams,
2746
+ onSuccess,
2747
+ onError
2748
+ }) as Promise<SuiTransactionBlockResponse>;
2749
+ }
2750
+
2751
+ /**
2752
+ * Query `WritesSettled` events from the Sui full node.
2753
+ *
2754
+ * Returns the most recent settlement events across ALL users of this DApp.
2755
+ * Each event captures one user's write debt settlement: number of writes,
2756
+ * bytes, and the amount charged (`paid_cost`).
2757
+ *
2758
+ * Note: Sui full nodes may prune old events. For long-term history use a
2759
+ * dedicated indexer table (`writes_settled_history`).
2760
+ *
2761
+ * @param limit Max events to return (default: 30, max: 50).
2762
+ * @param cursor Pagination cursor from a previous response.
2763
+ */
2764
+ async queryWritesSettledEvents(
2765
+ limit = 30,
2766
+ cursor?: { txDigest: string; eventSeq: string }
2767
+ ): Promise<
2768
+ Array<{
2769
+ txDigest: string;
2770
+ eventSeq: string;
2771
+ timestampMs: number;
2772
+ dappKey: string;
2773
+ account: string;
2774
+ writes: number;
2775
+ bytes: string;
2776
+ freeCost: string;
2777
+ paidCost: string;
2778
+ }>
2779
+ > {
2780
+ const fwPkg = this.frameworkPackageId;
2781
+ if (!fwPkg) return [];
2782
+ try {
2783
+ const resp = await this.suiInteractor.currentClient.queryEvents({
2784
+ query: { MoveEventType: `${fwPkg}::dubhe_events::WritesSettled` },
2785
+ cursor,
2786
+ limit: Math.min(limit, 50),
2787
+ order: 'descending'
2788
+ });
2789
+ return (resp.data ?? []).map((ev: any) => {
2790
+ const p = ev.parsedJson ?? {};
2791
+ return {
2792
+ txDigest: ev.id?.txDigest ?? '',
2793
+ eventSeq: ev.id?.eventSeq ?? '',
2794
+ timestampMs: Number(ev.timestampMs ?? 0),
2795
+ dappKey: p.dapp_key ?? '',
2796
+ account: p.account ?? '',
2797
+ writes: Number(p.writes ?? 0),
2798
+ bytes: String(p.bytes ?? '0'),
2799
+ freeCost: String(p.free_cost ?? '0'),
2800
+ paidCost: String(p.paid_cost ?? '0')
2801
+ };
2802
+ });
2803
+ } catch {
2804
+ return [];
2805
+ }
2806
+ }
2807
+
2808
+ /**
2809
+ * Query `MarketplaceFeeSettled` events from the Sui full node.
2810
+ *
2811
+ * Each event records the fee split for a completed marketplace purchase:
2812
+ * total fee, framework treasury share, and DApp revenue share.
2813
+ *
2814
+ * @param limit Max events to return (default: 30, max: 50).
2815
+ * @param cursor Pagination cursor from a previous response.
2816
+ */
2817
+ async queryMarketplaceFeeSettledEvents(
2818
+ limit = 30,
2819
+ cursor?: { txDigest: string; eventSeq: string }
2820
+ ): Promise<
2821
+ Array<{
2822
+ txDigest: string;
2823
+ eventSeq: string;
2824
+ timestampMs: number;
2825
+ dappKey: string;
2826
+ listingId: string;
2827
+ coinType: string;
2828
+ totalFee: string;
2829
+ treasuryAmount: string;
2830
+ dappAmount: string;
2831
+ }>
2832
+ > {
2833
+ const fwPkg = this.frameworkPackageId;
2834
+ if (!fwPkg) return [];
2835
+ try {
2836
+ const resp = await this.suiInteractor.currentClient.queryEvents({
2837
+ query: { MoveEventType: `${fwPkg}::dubhe_events::MarketplaceFeeSettled` },
2838
+ cursor,
2839
+ limit: Math.min(limit, 50),
2840
+ order: 'descending'
2841
+ });
2842
+ return (resp.data ?? []).map((ev: any) => {
2843
+ const p = ev.parsedJson ?? {};
2844
+ return {
2845
+ txDigest: ev.id?.txDigest ?? '',
2846
+ eventSeq: ev.id?.eventSeq ?? '',
2847
+ timestampMs: Number(ev.timestampMs ?? 0),
2848
+ dappKey: p.dapp_key ?? '',
2849
+ listingId: p.listing_id ?? '',
2850
+ coinType: p.coin_type ?? '',
2851
+ totalFee: String(p.total_fee ?? '0'),
2852
+ treasuryAmount: String(p.treasury_amount ?? '0'),
2853
+ dappAmount: String(p.dapp_amount ?? '0')
2854
+ };
2855
+ });
2856
+ } catch {
2857
+ return [];
2858
+ }
2859
+ }
2860
+
2861
+ /**
2862
+ * Read the DApp's accumulated revenue balance directly from the chain.
2863
+ *
2864
+ * `dapp_revenue` is stored as a dynamic field (`DappRevenueKey<CoinType>`)
2865
+ * on the DappStorage object — it is NOT a top-level field, so `getObject`
2866
+ * alone cannot return it. This method calls `getDynamicFields` to locate
2867
+ * the correct child object and returns its balance.
2868
+ *
2869
+ * Returns 0n when the dynamic field has never been created (no revenue yet).
2870
+ *
2871
+ * @param dappStorageId Object ID of the DApp's DappStorage shared object.
2872
+ * @param coinType Move type of the revenue coin (default: "0x2::sui::SUI").
2873
+ */
2874
+ async getDappRevenueBalance(dappStorageId: string, coinType = '0x2::sui::SUI'): Promise<bigint> {
2875
+ const client = this.suiInteractor.currentClient;
2876
+ // Normalise coinType to the bare module::name part for matching
2877
+ const coinTail = coinType.split('::').slice(-2).join('::'); // e.g. "sui::SUI"
2878
+ try {
2879
+ let cursor: string | null | undefined = undefined;
2880
+ do {
2881
+ const page = await client.getDynamicFields({
2882
+ parentId: dappStorageId,
2883
+ cursor,
2884
+ limit: 50
2885
+ });
2886
+ for (const field of page.data) {
2887
+ // The dynamic field type contains DappRevenueKey and the coin type
2888
+ const nameType: string = field.name?.type ?? '';
2889
+ if (nameType.includes('DappRevenueKey') && nameType.includes(coinTail)) {
2890
+ const fieldObj = await client.getDynamicFieldObject({
2891
+ parentId: dappStorageId,
2892
+ name: field.name
2893
+ });
2894
+ const innerFields = (fieldObj?.data?.content as any)?.fields ?? {};
2895
+ // Balance<T> is stored as { value: u64 }
2896
+ const balValue =
2897
+ innerFields?.value?.fields?.value ?? // nested Balance struct
2898
+ innerFields?.value ??
2899
+ 0;
2900
+ return BigInt(balValue);
2901
+ }
2902
+ }
2903
+ cursor = page.nextCursor;
2904
+ if (!page.hasNextPage) break;
2905
+ } while (cursor);
2906
+ } catch {
2907
+ // best-effort; return 0 on any RPC error
2908
+ }
2909
+ return 0n;
2910
+ }
2911
+
2912
+ /**
2913
+ * Withdraw all accumulated DApp revenue to the DApp admin address.
2914
+ *
2915
+ * Wraps `dapp_system::withdraw_dapp_revenue<DappKey, CoinType>`.
2916
+ * The entire revenue balance is transferred to the DApp admin on-chain.
2917
+ * Aborts if the balance is zero (`no_revenue_to_withdraw`).
2918
+ *
2919
+ * @param dappHubId Object ID of the DappHub shared object.
2920
+ * @param dappStorageId Object ID of the DApp's DappStorage shared object.
2921
+ * @param coinType Move type of the revenue coin (default: "0x2::sui::SUI").
2922
+ */
2923
+ async withdrawDappRevenue({
2924
+ dappHubId,
2925
+ dappStorageId: dappStorageIdParam,
2926
+ coinType = '0x2::sui::SUI',
2927
+ derivePathParams,
2928
+ onSuccess,
2929
+ onError
2930
+ }: {
2931
+ dappHubId: string;
2932
+ dappStorageId?: string;
2933
+ coinType?: string;
2934
+ derivePathParams?: DerivePathParams;
2935
+ onSuccess?: (result: SuiTransactionBlockResponse) => void | Promise<void>;
2936
+ onError?: (error: Error) => void | Promise<void>;
2937
+ }): Promise<SuiTransactionBlockResponse> {
2938
+ const fwPkg = this.frameworkPackageId;
2939
+ if (!fwPkg) {
2940
+ throw new Error(
2941
+ 'frameworkPackageId is required for withdrawDappRevenue. ' +
2942
+ 'Set it in the Dubhe constructor ({ frameworkPackageId: "0x..." }).'
2943
+ );
2944
+ }
2945
+ const storageId = dappStorageIdParam ?? this.dappStorageId;
2946
+ if (!storageId) {
2947
+ throw new Error(
2948
+ 'dappStorageId is required for withdrawDappRevenue. ' +
2949
+ 'Pass it directly or set it in the Dubhe constructor.'
2950
+ );
2951
+ }
2952
+ const typeArg = this.dappKey;
2953
+ if (!typeArg) {
2954
+ throw new Error(
2955
+ 'dappKey is required for withdrawDappRevenue. ' +
2956
+ 'Set it in the Dubhe constructor ({ packageId: "0x..." }).'
2957
+ );
2958
+ }
2959
+ const tx = new Transaction();
2960
+ tx.moveCall({
2961
+ target: `${fwPkg}::dapp_system::withdraw_dapp_revenue`,
2962
+ typeArguments: [typeArg, coinType],
2963
+ arguments: [tx.object(dappHubId), tx.object(storageId)]
2964
+ });
2965
+ return this.signAndSendTxn({
2966
+ tx,
2967
+ derivePathParams,
2968
+ onSuccess,
2969
+ onError
2970
+ }) as Promise<SuiTransactionBlockResponse>;
2971
+ }
2972
+
2973
+ /**
2974
+ * Return a combined settlement health snapshot for a user.
2975
+ *
2976
+ * Fetches `UserStorage` fields (always) and optionally `DappStorage` fields
2977
+ * (when `dappStorageId` is supplied) to produce high-level diagnostic values:
2978
+ *
2979
+ * - `utilizationPct` — how close the user is to the 2 000-write hard limit
2980
+ * - `isAtRisk` — utilization >= 80 % (settlement recommended soon)
2981
+ * - `isBlocked` — utilization >= 100 % (writes will be rejected)
2982
+ * - `estimatedWritesAffordable` — writes the credit pool can still cover
2983
+ * - `creditAtRisk` — fewer than 500 writes of credit remaining
2984
+ *
2985
+ * @param userStorageId Object ID of the user's UserStorage shared object.
2986
+ * @param dappStorageId Object ID of the DApp's DappStorage (optional).
2987
+ */
2988
+ async getSettlementHealth({
2989
+ userStorageId,
2990
+ dappStorageId
2991
+ }: {
2992
+ userStorageId: string;
2993
+ dappStorageId?: string;
2994
+ }): Promise<{
2995
+ unsettledCount: bigint;
2996
+ writeLimit: bigint;
2997
+ utilizationPct: number;
2998
+ isAtRisk: boolean;
2999
+ isBlocked: boolean;
3000
+ creditPool?: bigint;
3001
+ baseFeePerWrite?: bigint;
3002
+ estimatedWritesAffordable?: bigint;
3003
+ creditAtRisk?: boolean;
3004
+ }> {
3005
+ const WRITE_LIMIT = 2000n;
3006
+ const userFields = await this.getUserStorageFields(userStorageId);
3007
+ const unsettledCount = userFields.unsettled_count;
3008
+ const utilizationPct = Number((unsettledCount * 100n) / WRITE_LIMIT);
3009
+
3010
+ const result: {
3011
+ unsettledCount: bigint;
3012
+ writeLimit: bigint;
3013
+ utilizationPct: number;
3014
+ isAtRisk: boolean;
3015
+ isBlocked: boolean;
3016
+ creditPool?: bigint;
3017
+ baseFeePerWrite?: bigint;
3018
+ estimatedWritesAffordable?: bigint;
3019
+ creditAtRisk?: boolean;
3020
+ } = {
3021
+ unsettledCount,
3022
+ writeLimit: WRITE_LIMIT,
3023
+ utilizationPct,
3024
+ isAtRisk: utilizationPct >= 80,
3025
+ isBlocked: utilizationPct >= 100
3026
+ };
3027
+
3028
+ if (dappStorageId) {
3029
+ const dappFields = await this.getDappStorageFields(dappStorageId);
3030
+ const creditPool = dappFields.credit_pool;
3031
+ const baseFeePerWrite = dappFields.base_fee_per_write;
3032
+ const estimatedWritesAffordable =
3033
+ baseFeePerWrite > 0n ? creditPool / baseFeePerWrite : BigInt(Number.MAX_SAFE_INTEGER);
3034
+
3035
+ result.creditPool = creditPool;
3036
+ result.baseFeePerWrite = baseFeePerWrite;
3037
+ result.estimatedWritesAffordable = estimatedWritesAffordable;
3038
+ result.creditAtRisk = estimatedWritesAffordable < 500n;
3039
+ }
3040
+
3041
+ return result;
3042
+ }
3043
+
3044
+ // ─── Private helpers ─────────────────────────────────────────────────────────
3045
+
3046
+ /**
3047
+ * Return the default network configuration for the given network type.
3048
+ *
3049
+ * Useful for reading `frameworkPackageId` without instantiating a full client:
3050
+ *
3051
+ * ```ts
3052
+ * const { frameworkPackageId } = Dubhe.getDefaultConfig('testnet');
3053
+ * // → '0x8817b...' (known constant for testnet/mainnet)
3054
+ * // → undefined (for localnet/devnet — supply after local deployment)
3055
+ * ```
3056
+ */
3057
+ static getDefaultConfig(networkType: NetworkType): NetworkConfig {
3058
+ return getDefaultConfig(networkType);
3059
+ }
3060
+
2026
3061
  // async formatData(type: string, value: Buffer | number[] | Uint8Array) {
2027
3062
  // const u8Value = Uint8Array.from(value);
2028
3063
  // return bcs.de(type, u8Value);