@0xobelisk/sui-client 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/dist/dubhe.d.ts +417 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +838 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +832 -26
- package/dist/index.mjs.map +1 -1
- package/dist/libs/suiInteractor/defaultConfig.d.ts +26 -0
- package/dist/libs/suiInteractor/index.d.ts +1 -1
- package/dist/types/index.d.ts +14 -0
- package/dist/utils/index.d.ts +19 -1
- package/package.json +21 -21
- package/src/dubhe.ts +1058 -23
- package/src/index.ts +9 -0
- package/src/libs/suiInteractor/defaultConfig.ts +53 -8
- package/src/libs/suiInteractor/index.ts +1 -1
- package/src/types/index.ts +14 -0
- package/src/utils/index.ts +24 -1
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 {
|
|
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 =
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
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:
|
|
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);
|