@helium/helium-admin-cli 0.11.6 → 0.11.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/lib/cjs/claim-and-close-subscriber-key-to-assets.js +664 -0
  2. package/lib/cjs/claim-and-close-subscriber-key-to-assets.js.map +1 -0
  3. package/lib/cjs/close-all-subscriber-recipients.js +329 -0
  4. package/lib/cjs/close-all-subscriber-recipients.js.map +1 -0
  5. package/lib/cjs/close-legacy-auto-topoff.js +16 -11
  6. package/lib/cjs/close-legacy-auto-topoff.js.map +1 -1
  7. package/lib/cjs/extend-common-lut.js +20 -17
  8. package/lib/cjs/extend-common-lut.js.map +1 -1
  9. package/lib/cjs/issue-service-rewards-nft.js +112 -0
  10. package/lib/cjs/issue-service-rewards-nft.js.map +1 -0
  11. package/lib/cjs/reschedule-all-mini-fanouts.js +3 -5
  12. package/lib/cjs/reschedule-all-mini-fanouts.js.map +1 -1
  13. package/lib/cjs/setup-dc-auto-topoff.js +112 -49
  14. package/lib/cjs/setup-dc-auto-topoff.js.map +1 -1
  15. package/lib/cjs/utils.js +34 -18
  16. package/lib/cjs/utils.js.map +1 -1
  17. package/lib/esm/src/claim-and-close-subscriber-key-to-assets.js +616 -0
  18. package/lib/esm/src/claim-and-close-subscriber-key-to-assets.js.map +1 -0
  19. package/lib/esm/src/close-all-subscriber-recipients.js +287 -0
  20. package/lib/esm/src/close-all-subscriber-recipients.js.map +1 -0
  21. package/lib/esm/src/close-legacy-auto-topoff.js +19 -14
  22. package/lib/esm/src/close-legacy-auto-topoff.js.map +1 -1
  23. package/lib/esm/src/extend-common-lut.js +21 -18
  24. package/lib/esm/src/extend-common-lut.js.map +1 -1
  25. package/lib/esm/src/issue-service-rewards-nft.js +71 -0
  26. package/lib/esm/src/issue-service-rewards-nft.js.map +1 -0
  27. package/lib/esm/src/reschedule-all-mini-fanouts.js +3 -5
  28. package/lib/esm/src/reschedule-all-mini-fanouts.js.map +1 -1
  29. package/lib/esm/src/setup-dc-auto-topoff.js +121 -57
  30. package/lib/esm/src/setup-dc-auto-topoff.js.map +1 -1
  31. package/lib/esm/src/utils.js +35 -19
  32. package/lib/esm/src/utils.js.map +1 -1
  33. package/lib/esm/tsconfig.esm.tsbuildinfo +1 -1
  34. package/lib/types/src/claim-and-close-subscriber-key-to-assets.d.ts +2 -0
  35. package/lib/types/src/claim-and-close-subscriber-key-to-assets.d.ts.map +1 -0
  36. package/lib/types/src/close-all-subscriber-recipients.d.ts +2 -0
  37. package/lib/types/src/close-all-subscriber-recipients.d.ts.map +1 -0
  38. package/lib/types/src/close-legacy-auto-topoff.d.ts.map +1 -1
  39. package/lib/types/src/extend-common-lut.d.ts.map +1 -1
  40. package/lib/types/src/issue-service-rewards-nft.d.ts +2 -0
  41. package/lib/types/src/issue-service-rewards-nft.d.ts.map +1 -0
  42. package/lib/types/src/reschedule-all-mini-fanouts.d.ts.map +1 -1
  43. package/lib/types/src/setup-dc-auto-topoff.d.ts.map +1 -1
  44. package/lib/types/src/utils.d.ts +2 -1
  45. package/lib/types/src/utils.d.ts.map +1 -1
  46. package/package.json +15 -15
@@ -0,0 +1,664 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.run = void 0;
39
+ const anchor = __importStar(require("@coral-xyz/anchor"));
40
+ const client = __importStar(require("@helium/distributor-oracle"));
41
+ const helium_entity_manager_sdk_1 = require("@helium/helium-entity-manager-sdk");
42
+ const helium_sub_daos_sdk_1 = require("@helium/helium-sub-daos-sdk");
43
+ const lazy_distributor_sdk_1 = require("@helium/lazy-distributor-sdk");
44
+ const rewards_oracle_sdk_1 = require("@helium/rewards-oracle-sdk");
45
+ const mobile_entity_manager_sdk_1 = require("@helium/mobile-entity-manager-sdk");
46
+ const spl_utils_1 = require("@helium/spl-utils");
47
+ const web3_js_1 = require("@solana/web3.js");
48
+ const bn_js_1 = __importDefault(require("bn.js"));
49
+ const bs58_1 = __importDefault(require("bs58"));
50
+ const os_1 = __importDefault(require("os"));
51
+ const p_limit_1 = __importDefault(require("p-limit"));
52
+ const yargs_1 = __importDefault(require("yargs/yargs"));
53
+ const utils_1 = require("./utils");
54
+ // Hardcoded key_to_asset addresses from initial migration that need special handling
55
+ // These can be closed even if they have iot_info or mobile_info accounts
56
+ const HARDCODED_KEY_TO_ASSETS = [
57
+ new web3_js_1.PublicKey("AcKpRTmy6YKpQaWfLDBUaduQU1kHhNVLrPkW3TmEEqsc"),
58
+ new web3_js_1.PublicKey("3stUgrUq4j5BbamGdy7X2Y3dee24EeY5u1F7RHrrmaoP"),
59
+ new web3_js_1.PublicKey("4v7nfEN2Wj342Zm6V1Jwk9i5YCUHu6zBAJFENk6Gxzvr"),
60
+ new web3_js_1.PublicKey("2RtR6aVt6QgCSdV8LEH6ogWtDXGJpL73aB72DevJKgFC"),
61
+ ];
62
+ function run(args = process.argv) {
63
+ return __awaiter(this, void 0, void 0, function* () {
64
+ const yarg = (0, yargs_1.default)(args).options({
65
+ wallet: {
66
+ alias: "k",
67
+ describe: "Anchor wallet keypair",
68
+ default: `${os_1.default.homedir()}/.config/solana/id.json`,
69
+ },
70
+ url: {
71
+ alias: "u",
72
+ default: "http://127.0.0.1:8899",
73
+ describe: "The solana url",
74
+ },
75
+ authority: {
76
+ type: "string",
77
+ describe: "Path to the authority keypair. Defaults to wallet.",
78
+ },
79
+ commit: {
80
+ type: "boolean",
81
+ describe: "Actually claim and close accounts. Otherwise dry-run",
82
+ default: false,
83
+ },
84
+ });
85
+ const argv = yield yarg.argv;
86
+ process.env.ANCHOR_WALLET = argv.wallet;
87
+ process.env.ANCHOR_PROVIDER_URL = argv.url;
88
+ anchor.setProvider(anchor.AnchorProvider.local(argv.url));
89
+ const provider = anchor.getProvider();
90
+ const lazyProgram = yield (0, lazy_distributor_sdk_1.init)(provider);
91
+ const hemProgram = yield (0, helium_entity_manager_sdk_1.init)(provider);
92
+ const rewardsOracleProgram = yield (0, rewards_oracle_sdk_1.init)(provider);
93
+ const memProgram = yield (0, mobile_entity_manager_sdk_1.init)(provider);
94
+ const authority = argv.authority
95
+ ? (0, utils_1.loadKeypair)(argv.authority)
96
+ : (0, utils_1.loadKeypair)(argv.wallet);
97
+ const dao = (0, helium_sub_daos_sdk_1.daoKey)(spl_utils_1.HNT_MINT)[0];
98
+ const mobileSubDao = (0, helium_sub_daos_sdk_1.subDaoKey)(spl_utils_1.MOBILE_MINT)[0];
99
+ const iotSubDao = (0, helium_sub_daos_sdk_1.subDaoKey)(spl_utils_1.IOT_MINT)[0];
100
+ const mobileConfig = (0, helium_entity_manager_sdk_1.rewardableEntityConfigKey)(mobileSubDao, "MOBILE")[0];
101
+ const iotConfig = (0, helium_entity_manager_sdk_1.rewardableEntityConfigKey)(iotSubDao, "IOT")[0];
102
+ const lazyDistributor = (0, lazy_distributor_sdk_1.lazyDistributorKey)(spl_utils_1.MOBILE_MINT)[0];
103
+ // ========== STEP 1: Find all subscriber assets AND hardcoded key_to_assets ==========
104
+ console.log("=== STEP 1: FINDING KEY_TO_ASSET ACCOUNTS ===\n");
105
+ // Build list of key_to_asset info
106
+ const keyToAssetInfos = [];
107
+ // Process hardcoded key_to_assets FIRST
108
+ // Note: We fetch these to get assetId and asset data (for entityKey)
109
+ console.log(`Checking ${HARDCODED_KEY_TO_ASSETS.length} hardcoded accounts...`);
110
+ const hardcodedAccounts = yield Promise.all(HARDCODED_KEY_TO_ASSETS.map((keyToAssetAddr) => __awaiter(this, void 0, void 0, function* () {
111
+ try {
112
+ const keyToAssetAcc = yield hemProgram.account.keyToAssetV0.fetch(keyToAssetAddr);
113
+ return {
114
+ keyToAsset: keyToAssetAddr,
115
+ assetId: keyToAssetAcc.asset,
116
+ entityKey: Buffer.from(keyToAssetAcc.entityKey),
117
+ isHardcoded: true,
118
+ };
119
+ }
120
+ catch (err) {
121
+ return null; // Already closed
122
+ }
123
+ })));
124
+ keyToAssetInfos.push(...hardcodedAccounts.filter((a) => a !== null));
125
+ if (keyToAssetInfos.length > 0) {
126
+ console.log(` Found ${keyToAssetInfos.length} hardcoded key_to_assets\n`);
127
+ }
128
+ else {
129
+ console.log(` All hardcoded accounts already closed\n`);
130
+ }
131
+ // Find all subscriber assets using DAS
132
+ // Strategy: Fetch all MakerV0 accounts, identify subscriber collections, search by those
133
+ console.log("Finding all subscriber assets via DAS...");
134
+ const allSubscriberAssets = new Map(); // Dedupe by asset ID
135
+ try {
136
+ // Fetch all CarrierV0 accounts to get subscriber collections
137
+ console.log("Fetching CarrierV0 accounts...");
138
+ const carriers = yield memProgram.account.carrierV0.all();
139
+ const subscriberCollections = new Set();
140
+ for (const carrier of carriers) {
141
+ subscriberCollections.add(carrier.account.collection.toBase58());
142
+ }
143
+ console.log(` Found ${subscriberCollections.size} subscriber collection(s) from ${carriers.length} carriers`);
144
+ if (subscriberCollections.size === 0) {
145
+ console.log(" No subscriber collections found");
146
+ }
147
+ // Search each subscriber collection using cursor-based pagination
148
+ console.log("Searching DAS for subscriber assets...");
149
+ // Retry wrapper for DAS calls with exponential backoff
150
+ function getAssetsWithRetry(endpoint, params, maxRetries = 5) {
151
+ return __awaiter(this, void 0, void 0, function* () {
152
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
153
+ try {
154
+ return yield (0, spl_utils_1.getAssetsByGroup)(endpoint, params);
155
+ }
156
+ catch (err) {
157
+ if (attempt === maxRetries - 1) {
158
+ throw err;
159
+ }
160
+ // Exponential backoff: 2s, 4s, 8s, 16s, 30s
161
+ const delay = Math.min(2000 * Math.pow(2, attempt), 30000);
162
+ const errorMsg = err.message || err.toString();
163
+ console.log(` Error: ${errorMsg.substring(0, 80)}... retrying in ${delay / 1000}s (attempt ${attempt + 1}/${maxRetries})`);
164
+ yield new Promise((resolve) => setTimeout(resolve, delay));
165
+ }
166
+ }
167
+ throw new Error("Max retries exceeded");
168
+ });
169
+ }
170
+ for (const collection of subscriberCollections) {
171
+ const limit = 1000;
172
+ const startSize = allSubscriberAssets.size;
173
+ let cursor = undefined;
174
+ let fetchCount = 0;
175
+ const startTime = Date.now();
176
+ console.log(` Collection ${collection}: Starting...`);
177
+ while (true) {
178
+ const result = yield getAssetsWithRetry(provider.connection.rpcEndpoint, {
179
+ groupValue: collection,
180
+ limit,
181
+ cursor,
182
+ });
183
+ for (const asset of result.items) {
184
+ allSubscriberAssets.set(asset.id.toBase58(), asset);
185
+ }
186
+ fetchCount++;
187
+ const collectionTotal = allSubscriberAssets.size - startSize;
188
+ // Show progress: first 3 fetches, then every 25 fetches
189
+ if (fetchCount <= 3 || fetchCount % 25 === 0) {
190
+ console.log(` ${fetchCount} fetches: ${collectionTotal.toLocaleString()} assets`);
191
+ }
192
+ // Stop if no cursor returned (indicates end of data)
193
+ if (!result.cursor) {
194
+ break;
195
+ }
196
+ cursor = result.cursor;
197
+ // Small delay to avoid rate limits (100ms between fetches)
198
+ yield new Promise((resolve) => setTimeout(resolve, 100));
199
+ }
200
+ const thisCollectionTotal = allSubscriberAssets.size - startSize;
201
+ const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
202
+ console.log(` Complete: ${thisCollectionTotal.toLocaleString()} assets in ${fetchCount} fetches (${totalTime}s, cumulative: ${allSubscriberAssets.size.toLocaleString()})`);
203
+ }
204
+ console.log(`\nTotal unique subscriber assets found: ${allSubscriberAssets.size}`);
205
+ console.log("Processing assets into key_to_asset accounts...");
206
+ const existingKeyToAssetSet = new Set(keyToAssetInfos.map((k) => k.keyToAsset.toBase58()));
207
+ let processed = 0;
208
+ for (const asset of allSubscriberAssets.values()) {
209
+ const keyToAssetAddr = (0, helium_entity_manager_sdk_1.keyToAssetForAsset)(asset, dao);
210
+ // Ensure keyToAssetAddr is a PublicKey
211
+ const keyToAssetPubkey = typeof keyToAssetAddr === "string"
212
+ ? new web3_js_1.PublicKey(keyToAssetAddr)
213
+ : keyToAssetAddr;
214
+ // Skip if already in list (from hardcoded)
215
+ const keyToAssetStr = keyToAssetPubkey.toBase58();
216
+ if (!existingKeyToAssetSet.has(keyToAssetStr)) {
217
+ keyToAssetInfos.push({
218
+ keyToAsset: keyToAssetPubkey,
219
+ assetId: asset.id,
220
+ asset: asset,
221
+ isHardcoded: false,
222
+ });
223
+ existingKeyToAssetSet.add(keyToAssetStr);
224
+ }
225
+ processed++;
226
+ // Show progress every 100k assets
227
+ if (processed % 100000 === 0) {
228
+ console.log(` Processed ${processed.toLocaleString()} / ${allSubscriberAssets.size.toLocaleString()} assets...`);
229
+ }
230
+ }
231
+ console.log(` Processed all ${allSubscriberAssets.size.toLocaleString()} assets\n`);
232
+ // Free memory - no longer need full asset objects or dedup set
233
+ allSubscriberAssets.clear();
234
+ existingKeyToAssetSet.clear();
235
+ }
236
+ catch (err) {
237
+ console.log(`\n⚠️ DAS search failed: ${err.message}`);
238
+ console.log(` Continuing with ${keyToAssetInfos.length} hardcoded key_to_assets only.`);
239
+ }
240
+ const hardcodedCount = keyToAssetInfos.filter((k) => k.isHardcoded).length;
241
+ const subscriberCount = keyToAssetInfos.filter((k) => !k.isHardcoded).length;
242
+ console.log(`\nTotal: ${keyToAssetInfos.length.toLocaleString()} key_to_asset accounts (${hardcodedCount} hardcoded, ${subscriberCount.toLocaleString()} subscribers)`);
243
+ // Extract entity keys (try URI first, fetch as fallback)
244
+ console.log("Extracting entity keys from asset data...");
245
+ const existingKeyToAssets = [];
246
+ const needsFetch = [];
247
+ // Try to extract from URI first
248
+ keyToAssetInfos.forEach((info) => {
249
+ var _a, _b;
250
+ if (info.entityKey) {
251
+ // Hardcoded account - already have entity key
252
+ existingKeyToAssets.push({
253
+ keyToAsset: info.keyToAsset,
254
+ assetId: info.assetId,
255
+ entityKey: info.entityKey,
256
+ isHardcoded: info.isHardcoded,
257
+ });
258
+ }
259
+ else if ((_b = (_a = info.asset) === null || _a === void 0 ? void 0 : _a.content) === null || _b === void 0 ? void 0 : _b.json_uri) {
260
+ // Try to extract from URI
261
+ const entityKeyStr = info.asset.content.json_uri.split("/").slice(-1)[0];
262
+ const cleanEntityKeyStr = entityKeyStr
263
+ .split(".")[0]
264
+ .split("?")[0]
265
+ .split("#")[0];
266
+ try {
267
+ const entityKey = Buffer.from(bs58_1.default.decode(cleanEntityKeyStr));
268
+ existingKeyToAssets.push({
269
+ keyToAsset: info.keyToAsset,
270
+ assetId: info.assetId,
271
+ entityKey,
272
+ isHardcoded: info.isHardcoded,
273
+ });
274
+ }
275
+ catch (err) {
276
+ // URI extraction failed - need to fetch
277
+ needsFetch.push(info);
278
+ }
279
+ }
280
+ else {
281
+ // No URI - need to fetch
282
+ needsFetch.push(info);
283
+ }
284
+ });
285
+ console.log(` Extracted ${existingKeyToAssets.length.toLocaleString()} entity keys from URIs`);
286
+ // Fetch the ones that failed URI extraction
287
+ if (needsFetch.length > 0) {
288
+ console.log(` Fetching ${needsFetch.length.toLocaleString()} keyToAssetV0 accounts (URI extraction failed)...`);
289
+ function fetchKeyToAssetsWithRetry(chunk, maxRetries = 3) {
290
+ return __awaiter(this, void 0, void 0, function* () {
291
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
292
+ try {
293
+ const accountInfos = yield hemProgram.account.keyToAssetV0.fetchMultiple(chunk.map((k) => k.keyToAsset));
294
+ return { accountInfos, chunk };
295
+ }
296
+ catch (err) {
297
+ if (attempt === maxRetries - 1)
298
+ throw err;
299
+ const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
300
+ yield new Promise((resolve) => setTimeout(resolve, delay));
301
+ }
302
+ }
303
+ throw new Error("Max retries exceeded");
304
+ });
305
+ }
306
+ const keyToAssetChunks = (0, spl_utils_1.chunks)(needsFetch, 100);
307
+ const keyToAssetLimiter = (0, p_limit_1.default)(10);
308
+ let keyToAssetFetchedCount = 0;
309
+ let keyToAssetBatchIndex = 0;
310
+ const keyToAssetBatchResults = yield Promise.all(keyToAssetChunks.map((chunk) => keyToAssetLimiter(() => __awaiter(this, void 0, void 0, function* () {
311
+ const result = yield fetchKeyToAssetsWithRetry(chunk);
312
+ keyToAssetFetchedCount += chunk.length;
313
+ keyToAssetBatchIndex++;
314
+ // Show progress
315
+ if (keyToAssetBatchIndex <= 3 ||
316
+ keyToAssetBatchIndex % 10 === 0 ||
317
+ keyToAssetFetchedCount === needsFetch.length) {
318
+ console.log(` ${keyToAssetBatchIndex} batches: ${keyToAssetFetchedCount.toLocaleString()} accounts`);
319
+ }
320
+ return result;
321
+ }))));
322
+ // Process fetched accounts
323
+ keyToAssetBatchResults.forEach(({ accountInfos, chunk }) => {
324
+ accountInfos.forEach((account, index) => {
325
+ if (account) {
326
+ existingKeyToAssets.push({
327
+ keyToAsset: chunk[index].keyToAsset,
328
+ assetId: chunk[index].assetId,
329
+ entityKey: Buffer.from(account.entityKey),
330
+ isHardcoded: chunk[index].isHardcoded,
331
+ });
332
+ }
333
+ });
334
+ });
335
+ console.log(` Fetched ${keyToAssetFetchedCount.toLocaleString()} entity keys from accounts`);
336
+ }
337
+ console.log(` Total: ${existingKeyToAssets.length.toLocaleString()} entity keys\n`);
338
+ // Free memory - no longer need original key_to_asset infos or assets
339
+ keyToAssetInfos.length = 0;
340
+ // ========== STEP 2: Claim all rewards until none remain ==========
341
+ console.log("\n=== STEP 2: CLAIMING REWARDS ===\n");
342
+ let claimIteration = 0;
343
+ let totalClaimedAmount = new bn_js_1.default(0);
344
+ let totalClaimedTransactions = 0;
345
+ // Build recipient lookup
346
+ console.log(`Fetching ${existingKeyToAssets.length.toLocaleString()} recipient accounts...`);
347
+ const existingRecipients = new Set();
348
+ const recipientKeys = existingKeyToAssets.map(({ assetId }) => (0, lazy_distributor_sdk_1.recipientKey)(lazyDistributor, assetId)[0]);
349
+ function fetchRecipientsWithRetry(chunk, maxRetries = 3) {
350
+ return __awaiter(this, void 0, void 0, function* () {
351
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
352
+ try {
353
+ const accountInfos = yield lazyProgram.account.recipientV0.fetchMultiple(chunk);
354
+ return { accountInfos, chunk };
355
+ }
356
+ catch (err) {
357
+ if (attempt === maxRetries - 1)
358
+ throw err;
359
+ const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
360
+ yield new Promise((resolve) => setTimeout(resolve, delay));
361
+ }
362
+ }
363
+ throw new Error("Max retries exceeded");
364
+ });
365
+ }
366
+ // Fetch recipients with concurrency limit
367
+ const recipientChunks = (0, spl_utils_1.chunks)(recipientKeys, 1000);
368
+ const limiter = (0, p_limit_1.default)(10);
369
+ let fetchedCount = 0;
370
+ let batchIndex = 0;
371
+ // Build assetsWithRecipients directly while fetching (no extra loops!)
372
+ const assetsWithRecipients = [];
373
+ yield Promise.all(recipientChunks.map((chunk, chunkIndex) => limiter(() => __awaiter(this, void 0, void 0, function* () {
374
+ const result = yield fetchRecipientsWithRetry(chunk);
375
+ fetchedCount += chunk.length;
376
+ batchIndex++;
377
+ // Calculate starting index for this chunk
378
+ const chunkStartIndex = chunkIndex * 1000;
379
+ // Build assetsWithRecipients directly from results
380
+ result.accountInfos.forEach((info, index) => {
381
+ if (info) {
382
+ const recipientAddr = chunk[index].toBase58();
383
+ existingRecipients.add(recipientAddr);
384
+ // Use index to get corresponding asset (no map lookup!)
385
+ assetsWithRecipients.push(existingKeyToAssets[chunkStartIndex + index]);
386
+ }
387
+ });
388
+ // Show progress: first 3 batches, then every 25 batches
389
+ if (batchIndex <= 3 ||
390
+ batchIndex % 25 === 0 ||
391
+ fetchedCount === recipientKeys.length) {
392
+ console.log(` ${batchIndex} batches: ${fetchedCount.toLocaleString()} accounts`);
393
+ }
394
+ return result;
395
+ }))));
396
+ console.log(` Found ${existingRecipients.size.toLocaleString()} existing recipients\n`);
397
+ console.log(`Checking rewards for ${assetsWithRecipients.length.toLocaleString()} assets with recipients (skipping ${(existingKeyToAssets.length - assetsWithRecipients.length).toLocaleString()} without recipients)\n`);
398
+ while (true) {
399
+ claimIteration++;
400
+ console.log(`--- Claim Iteration ${claimIteration} ---`);
401
+ // Check pending rewards using bulk API
402
+ let assetsWithRewards = 0;
403
+ let assetsWithZeroRewards = 0;
404
+ let totalPendingRewards = new bn_js_1.default(0);
405
+ const assetsToClaim = [];
406
+ // Create map of entityKey -> asset info for lookup
407
+ const entityKeyToAsset = new Map();
408
+ assetsWithRecipients.forEach(({ assetId, entityKey }) => {
409
+ // Decode entity key to string format that oracle expects (b58 for subscriber assets)
410
+ const entityKeyStr = (0, helium_entity_manager_sdk_1.decodeEntityKey)(entityKey, { b58: {} });
411
+ if (entityKeyStr) {
412
+ entityKeyToAsset.set(entityKeyStr, {
413
+ assetId,
414
+ entityKey,
415
+ });
416
+ }
417
+ });
418
+ // Get bulk rewards in chunks
419
+ const entityKeys = Array.from(entityKeyToAsset.keys());
420
+ const rewardsChunks = (0, spl_utils_1.chunks)(entityKeys, 5000);
421
+ const rewardsLimiter = (0, p_limit_1.default)(5);
422
+ let checkedCount = 0;
423
+ let rewardsBatchIndex = 0;
424
+ const bulkRewardsResults = yield Promise.all(rewardsChunks.map((chunk) => rewardsLimiter(() => __awaiter(this, void 0, void 0, function* () {
425
+ try {
426
+ const bulkRewards = yield client.getBulkRewards(lazyProgram, lazyDistributor, chunk);
427
+ rewardsBatchIndex++;
428
+ checkedCount += chunk.length;
429
+ // Show progress
430
+ if (rewardsBatchIndex <= 3 ||
431
+ rewardsBatchIndex % 5 === 0 ||
432
+ checkedCount === entityKeys.length) {
433
+ console.log(` ${rewardsBatchIndex} batches: ${checkedCount.toLocaleString()} assets`);
434
+ }
435
+ return { chunk, bulkRewards };
436
+ }
437
+ catch (err) {
438
+ console.error(`Error fetching bulk rewards: ${err.message}`);
439
+ return { chunk, bulkRewards: [] };
440
+ }
441
+ }))));
442
+ // Merge bulk rewards from all chunks
443
+ // Each chunk has the same oracle keys but different entity key data
444
+ const mergedBulkRewards = [];
445
+ if (bulkRewardsResults.length > 0 &&
446
+ bulkRewardsResults[0].bulkRewards.length > 0) {
447
+ const numOracles = bulkRewardsResults[0].bulkRewards.length;
448
+ for (let oracleIdx = 0; oracleIdx < numOracles; oracleIdx++) {
449
+ const mergedRewards = {};
450
+ // Merge rewards from all chunks for this oracle
451
+ bulkRewardsResults.forEach(({ bulkRewards }) => {
452
+ if (bulkRewards[oracleIdx]) {
453
+ Object.assign(mergedRewards, bulkRewards[oracleIdx].currentRewards);
454
+ }
455
+ });
456
+ mergedBulkRewards.push({
457
+ currentRewards: mergedRewards,
458
+ oracleKey: bulkRewardsResults[0].bulkRewards[oracleIdx].oracleKey,
459
+ });
460
+ }
461
+ }
462
+ // Process rewards using merged bulk rewards
463
+ entityKeys.forEach((entityKey) => {
464
+ const assetInfo = entityKeyToAsset.get(entityKey);
465
+ if (!assetInfo)
466
+ return;
467
+ // Aggregate rewards across all oracles for this entity
468
+ const rewards = mergedBulkRewards.map((oracle) => ({
469
+ currentRewards: oracle.currentRewards[entityKey] || "0",
470
+ oracleKey: oracle.oracleKey,
471
+ }));
472
+ const totalRewards = rewards.reduce((sum, r) => {
473
+ return sum.add(new bn_js_1.default(r.currentRewards));
474
+ }, new bn_js_1.default(0));
475
+ if (totalRewards.gt(new bn_js_1.default(0))) {
476
+ assetsWithRewards++;
477
+ totalPendingRewards = totalPendingRewards.add(totalRewards);
478
+ assetsToClaim.push({
479
+ asset: assetInfo.assetId,
480
+ rewards,
481
+ });
482
+ }
483
+ else {
484
+ assetsWithZeroRewards++;
485
+ }
486
+ });
487
+ const pendingMobile = totalPendingRewards.div(new bn_js_1.default(100000000)).toString();
488
+ console.log(`\nPending: ${assetsWithRewards} assets with ${pendingMobile} MOBILE (${assetsWithZeroRewards} with zero)`);
489
+ // If no more rewards, break out of loop
490
+ if (assetsWithRewards === 0) {
491
+ console.log("\n✓ All rewards have been claimed!");
492
+ break;
493
+ }
494
+ if (!argv.commit) {
495
+ console.log(`\nDry run: would claim rewards for ${assetsWithRewards} assets`);
496
+ break;
497
+ }
498
+ console.log(`\nClaiming rewards for ${assetsWithRewards} assets...`);
499
+ // Batch claim rewards using formBulkTransactions (up to 100 assets per call)
500
+ console.log(`Preparing ${assetsToClaim.length} claim transactions...`);
501
+ const claimChunks = (0, spl_utils_1.chunks)(assetsToClaim, 100); // formBulkTransactions supports up to 100
502
+ const claimTxLimiter = (0, p_limit_1.default)(10);
503
+ let preparedCount = 0;
504
+ const allBatchTxns = yield Promise.all(claimChunks.map((chunk) => claimTxLimiter(() => __awaiter(this, void 0, void 0, function* () {
505
+ try {
506
+ const assets = chunk.map((c) => c.asset);
507
+ // Use formBulkTransactions which is more efficient than individual formTransaction calls
508
+ const txns = yield client.formBulkTransactions({
509
+ program: lazyProgram,
510
+ rewardsOracleProgram: rewardsOracleProgram,
511
+ rewards: mergedBulkRewards,
512
+ assets,
513
+ lazyDistributor,
514
+ wallet: authority.publicKey,
515
+ });
516
+ preparedCount += chunk.length;
517
+ // Show progress every 100 or at end
518
+ if (preparedCount % 100 === 0 ||
519
+ preparedCount === assetsToClaim.length) {
520
+ console.log(` Prepared ${preparedCount}/${assetsToClaim.length} transactions`);
521
+ }
522
+ return txns;
523
+ }
524
+ catch (err) {
525
+ console.error(`Error forming bulk transactions for batch: ${err.message}`);
526
+ return [];
527
+ }
528
+ }))));
529
+ const txns = allBatchTxns.flat().filter(spl_utils_1.truthy);
530
+ console.log(`Sending ${txns.length} transactions...`);
531
+ for (const tx of txns) {
532
+ tx.sign([authority]);
533
+ }
534
+ yield (0, spl_utils_1.bulkSendRawTransactions)(provider.connection, txns.map((tx) => Buffer.from(tx.serialize())), (status) => {
535
+ // Only show every 10 batches for mainnet scale
536
+ if (status.totalProgress % 10 === 0 ||
537
+ status.totalProgress === txns.length) {
538
+ console.log(` Sent ${status.totalProgress} / ${txns.length}`);
539
+ }
540
+ });
541
+ totalClaimedAmount = totalClaimedAmount.add(totalPendingRewards);
542
+ totalClaimedTransactions += txns.length;
543
+ console.log(`Iteration ${claimIteration} complete. Claimed ${totalPendingRewards
544
+ .div(new bn_js_1.default(100000000))
545
+ .toString()} MOBILE across ${txns.length} transactions.`);
546
+ // Free memory for next iteration
547
+ assetsToClaim.length = 0;
548
+ txns.length = 0;
549
+ // Wait a bit before next iteration to let chain settle
550
+ console.log("\nWaiting 5 seconds before next check...");
551
+ yield new Promise((resolve) => setTimeout(resolve, 5000));
552
+ }
553
+ const claimedMobile = totalClaimedAmount.div(new bn_js_1.default(100000000)).toString();
554
+ console.log(`\nClaimed ${claimedMobile} MOBILE in ${claimIteration} iteration(s), ${totalClaimedTransactions} tx(s)`);
555
+ // Free memory - no longer need recipient data
556
+ existingRecipients.clear();
557
+ recipientKeys.length = 0;
558
+ // ========== STEP 3: Close all KeyToAssetV0 accounts ==========
559
+ console.log("\n=== STEP 3: CLOSING ACCOUNTS ===\n");
560
+ if (!argv.commit) {
561
+ const closingHardcoded = existingKeyToAssets.filter((k) => k.isHardcoded).length;
562
+ const closingSubscribers = existingKeyToAssets.filter((k) => !k.isHardcoded).length;
563
+ console.log(`Dry run: would close ${existingKeyToAssets.length} accounts (${closingHardcoded} hardcoded, ${closingSubscribers} subscribers)`);
564
+ console.log(`\nRe-run with --commit to execute`);
565
+ console.log(`Then run close-all-subscriber-recipients to close RecipientV0 accounts`);
566
+ return;
567
+ }
568
+ console.log(`Closing ${existingKeyToAssets.length.toLocaleString()} accounts...`);
569
+ // Build close instructions
570
+ const instructions = [];
571
+ // Separate hardcoded (need info checks) from subscribers (no checks needed)
572
+ const hardcodedAccountsToClose = existingKeyToAssets.filter((k) => k.isHardcoded);
573
+ const subscriberAccounts = existingKeyToAssets.filter((k) => !k.isHardcoded);
574
+ console.log(` Hardcoded: ${hardcodedAccountsToClose.length}, Subscribers: ${subscriberAccounts.length.toLocaleString()}`);
575
+ // Process hardcoded accounts (check info accounts)
576
+ if (hardcodedAccountsToClose.length > 0) {
577
+ console.log("Processing hardcoded accounts...");
578
+ for (const { keyToAsset, assetId, entityKey } of hardcodedAccountsToClose) {
579
+ const [mobileInfo] = (0, helium_entity_manager_sdk_1.mobileInfoKey)(mobileConfig, entityKey);
580
+ const [iotInfo] = (0, helium_entity_manager_sdk_1.iotInfoKey)(iotConfig, entityKey);
581
+ const [mobileInfoAcc, iotInfoAcc] = yield Promise.all([
582
+ provider.connection.getAccountInfo(mobileInfo),
583
+ provider.connection.getAccountInfo(iotInfo),
584
+ ]);
585
+ console.log(` ${keyToAsset.toBase58()}`);
586
+ console.log(` Mobile Info: ${mobileInfoAcc ? "exists (will close)" : "does not exist"}`);
587
+ console.log(` IoT Info: ${iotInfoAcc ? "exists (will close)" : "does not exist"}`);
588
+ const ix = yield hemProgram.methods
589
+ .tempCloseKeyToAssetV0()
590
+ .accountsPartial({
591
+ keyToAsset,
592
+ dao,
593
+ authority: authority.publicKey,
594
+ asset: assetId,
595
+ mobileConfig,
596
+ iotConfig,
597
+ mobileInfo,
598
+ iotInfo,
599
+ iotSubDao,
600
+ mobileSubDao,
601
+ })
602
+ .instruction();
603
+ instructions.push(ix);
604
+ }
605
+ }
606
+ // Process subscriber accounts (no info checks - they shouldn't have info accounts)
607
+ if (subscriberAccounts.length > 0) {
608
+ console.log(`Processing ${subscriberAccounts.length.toLocaleString()} subscriber accounts...`);
609
+ let processed = 0;
610
+ const subscriberChunks = (0, spl_utils_1.chunks)(subscriberAccounts, 100);
611
+ const instructionLimiter = (0, p_limit_1.default)(50);
612
+ const allBatchInstructions = yield Promise.all(subscriberChunks.map((chunk) => instructionLimiter(() => __awaiter(this, void 0, void 0, function* () {
613
+ const batchInstructions = yield Promise.all(chunk.map(({ keyToAsset, assetId, entityKey }) => __awaiter(this, void 0, void 0, function* () {
614
+ const [mobileInfo] = (0, helium_entity_manager_sdk_1.mobileInfoKey)(mobileConfig, entityKey);
615
+ const [iotInfo] = (0, helium_entity_manager_sdk_1.iotInfoKey)(iotConfig, entityKey);
616
+ return yield hemProgram.methods
617
+ .tempCloseKeyToAssetV0()
618
+ .accountsPartial({
619
+ keyToAsset,
620
+ dao,
621
+ authority: authority.publicKey,
622
+ asset: assetId,
623
+ mobileConfig,
624
+ iotConfig,
625
+ mobileInfo,
626
+ iotInfo,
627
+ iotSubDao,
628
+ mobileSubDao,
629
+ })
630
+ .instruction();
631
+ })));
632
+ processed += chunk.length;
633
+ // Show progress every 10k instructions
634
+ if (processed % 10000 === 0 ||
635
+ processed === subscriberAccounts.length) {
636
+ console.log(` Prepared ${processed.toLocaleString()} / ${subscriberAccounts.length.toLocaleString()} instructions...`);
637
+ }
638
+ return batchInstructions;
639
+ }))));
640
+ instructions.push(...allBatchInstructions.flat().filter(spl_utils_1.truthy));
641
+ }
642
+ console.log(`\nBatching ${instructions.length.toLocaleString()} instructions into transactions...`);
643
+ // Free memory - no longer need existingKeyToAssets
644
+ existingKeyToAssets.length = 0;
645
+ const closeTxns = yield (0, spl_utils_1.batchInstructionsToTxsWithPriorityFee)(provider, instructions, {
646
+ useFirstEstimateForAll: true,
647
+ computeUnitLimit: 600000,
648
+ });
649
+ // Free memory - instructions now in transactions
650
+ instructions.length = 0;
651
+ console.log(`\nSending ${closeTxns.length} transactions...`);
652
+ // Sign with authority and send
653
+ yield (0, spl_utils_1.bulkSendTransactions)(provider, closeTxns, (status) => {
654
+ console.log(`Sending ${status.currentBatchProgress} / ${status.currentBatchSize} in batch. ${status.totalProgress} / ${closeTxns.length}`);
655
+ }, 10, [authority]);
656
+ const finalClaimedMobile = totalClaimedAmount
657
+ .div(new bn_js_1.default(100000000))
658
+ .toString();
659
+ console.log(`\n✓ Complete: Claimed ${finalClaimedMobile} MOBILE, closed ${instructions.length} accounts`);
660
+ console.log(`Next: run close-all-subscriber-recipients`);
661
+ });
662
+ }
663
+ exports.run = run;
664
+ //# sourceMappingURL=claim-and-close-subscriber-key-to-assets.js.map