2020-10-19 02:26:14 +03:00
|
|
|
import configs from "~/knexfile";
|
|
|
|
import knex from "knex";
|
2020-09-16 11:40:03 +03:00
|
|
|
import fs from "fs-extra";
|
2020-10-19 02:26:14 +03:00
|
|
|
import "isomorphic-fetch";
|
2020-09-16 11:40:03 +03:00
|
|
|
|
|
|
|
import * as Environment from "~/node_common/environment";
|
|
|
|
import * as Data from "~/node_common/data";
|
|
|
|
import * as Utilities from "~/node_common/utilities";
|
|
|
|
import * as Strings from "~/common/strings";
|
|
|
|
import * as Logs from "~/node_common/script-logging";
|
2020-10-19 02:26:14 +03:00
|
|
|
import * as Serializers from "~/node_common/serializers";
|
2020-09-16 11:40:03 +03:00
|
|
|
|
|
|
|
import { Buckets, PrivateKey } from "@textile/hub";
|
|
|
|
import { v4 as uuid } from "uuid";
|
|
|
|
|
2020-10-19 02:26:14 +03:00
|
|
|
const envConfig = configs["development"];
|
|
|
|
const db = knex(envConfig);
|
|
|
|
|
2020-10-21 02:59:38 +03:00
|
|
|
// 64 MB minimum
|
|
|
|
const MINIMUM_BYTES_CONSIDERATION = 67108864;
|
2020-10-19 08:08:02 +03:00
|
|
|
// 100 MB minimum
|
|
|
|
const MINIMUM_BYTES_FOR_STORAGE = 104857600;
|
2020-09-16 11:40:03 +03:00
|
|
|
const STORAGE_BOT_NAME = "STORAGE WORKER";
|
2020-10-19 20:03:22 +03:00
|
|
|
|
|
|
|
// We don't make new buckets if they have more than 10.
|
|
|
|
const BUCKET_LIMIT = 10;
|
2020-09-16 11:40:03 +03:00
|
|
|
const PRACTICE_RUN = false;
|
2020-10-19 02:26:14 +03:00
|
|
|
const SKIP_NEW_BUCKET_CREATION = false;
|
2020-10-18 21:10:42 +03:00
|
|
|
const STORE_MEANINGFUL_ADDRESS_ONLY_AND_PERFORM_NO_ACTIONS = false;
|
2020-10-19 02:26:14 +03:00
|
|
|
const WRITE_TO_SLATE_STORAGE_DEAL_INDEX = true;
|
2020-09-16 11:40:03 +03:00
|
|
|
|
|
|
|
const TEXTILE_KEY_INFO = {
|
|
|
|
key: Environment.TEXTILE_HUB_KEY,
|
|
|
|
secret: Environment.TEXTILE_HUB_SECRET,
|
|
|
|
};
|
|
|
|
|
|
|
|
console.log(`RUNNING: worker-heavy-stones.js`);
|
|
|
|
|
2020-10-18 21:10:42 +03:00
|
|
|
const delay = async (waitMs) => {
|
|
|
|
return await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
|
|
};
|
|
|
|
|
2020-10-19 02:26:14 +03:00
|
|
|
const minerMap = {};
|
|
|
|
|
2020-09-16 11:40:03 +03:00
|
|
|
const run = async () => {
|
2020-10-19 02:26:14 +03:00
|
|
|
Logs.taskTimeless(`Fetching every miner ...`);
|
|
|
|
|
|
|
|
const minerData = await fetch("https://sentinel.slate.host/api/mapped-static-global-miners");
|
|
|
|
const jsonData = await minerData.json();
|
|
|
|
|
|
|
|
jsonData.data.forEach((group) => {
|
|
|
|
group.minerAddresses.forEach((entity) => {
|
2020-10-21 02:59:38 +03:00
|
|
|
minerMap[entity.id] = entity;
|
|
|
|
minerMap[entity.id.replace("t", "f")] = entity;
|
2020-10-19 02:26:14 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
Logs.taskTimeless(`Fetching every user ...`);
|
2020-09-16 11:40:03 +03:00
|
|
|
const response = await Data.getEveryUser(false);
|
|
|
|
|
2020-10-20 01:37:13 +03:00
|
|
|
let storageUsers = [];
|
2020-09-16 11:40:03 +03:00
|
|
|
let bytes = 0;
|
|
|
|
let dealUsers = 0;
|
|
|
|
let totalUsers = 0;
|
|
|
|
let encryptedUsers = 0;
|
|
|
|
|
|
|
|
// NOTE(jim): Only users who agree. Opt in by default.
|
|
|
|
for (let i = 0; i < response.length; i++) {
|
|
|
|
const user = response[i];
|
|
|
|
|
|
|
|
if (user.data.allow_automatic_data_storage) {
|
2020-10-20 01:37:13 +03:00
|
|
|
storageUsers.unshift(user);
|
2020-09-16 11:40:03 +03:00
|
|
|
dealUsers = dealUsers + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (user.data.allow_encrypted_data_storage) {
|
|
|
|
encryptedUsers = encryptedUsers + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
totalUsers = totalUsers + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < storageUsers.length; i++) {
|
|
|
|
const user = storageUsers[i];
|
|
|
|
const printData = {
|
|
|
|
username: storageUsers[i].username,
|
|
|
|
slateURL: `https://slate.host/${storageUsers[i].username}`,
|
|
|
|
isForcingEncryption: user.data.allow_encrypted_data_storage,
|
|
|
|
};
|
|
|
|
let buckets;
|
|
|
|
|
2020-10-19 02:26:14 +03:00
|
|
|
await delay(500);
|
2020-10-18 21:10:42 +03:00
|
|
|
|
2020-09-16 11:40:03 +03:00
|
|
|
try {
|
|
|
|
const token = user.data.tokens.api;
|
|
|
|
const identity = await PrivateKey.fromString(token);
|
|
|
|
buckets = await Buckets.withKeyInfo(TEXTILE_KEY_INFO);
|
|
|
|
await buckets.getToken(identity);
|
|
|
|
buckets = await Utilities.setupWithThread({ buckets });
|
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
}
|
|
|
|
|
|
|
|
let userBuckets = [];
|
|
|
|
try {
|
|
|
|
userBuckets = await buckets.list();
|
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
}
|
|
|
|
|
|
|
|
let userBytes = 0;
|
|
|
|
|
|
|
|
for (let k = 0; k < userBuckets.length; k++) {
|
|
|
|
try {
|
|
|
|
const path = await buckets.listPath(userBuckets[k].key, "/");
|
|
|
|
const data = path.item;
|
|
|
|
|
|
|
|
if (data.name !== "data") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
userBuckets[k].bucketSize = data.size;
|
|
|
|
userBytes = userBytes + data.size;
|
|
|
|
bytes = bytes + userBytes;
|
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE(jim): Skip people.
|
2020-10-21 02:59:38 +03:00
|
|
|
if (userBytes < MINIMUM_BYTES_CONSIDERATION) {
|
|
|
|
Logs.note(`SKIP: ${user.username}, they only have ${Strings.bytesToSize(userBytes)}`);
|
2020-09-16 11:40:03 +03:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-10-21 02:59:38 +03:00
|
|
|
printData.bytes = userBytes;
|
|
|
|
|
2020-09-16 11:40:03 +03:00
|
|
|
const PowergateSingleton = await Utilities.getPowergateAPIFromUserToken({
|
|
|
|
user,
|
|
|
|
});
|
|
|
|
const { powerInfo, power } = PowergateSingleton;
|
|
|
|
let balance = 0;
|
|
|
|
let address = null;
|
|
|
|
|
2020-10-19 02:26:14 +03:00
|
|
|
await delay(500);
|
2020-10-18 21:10:42 +03:00
|
|
|
|
2020-09-16 11:40:03 +03:00
|
|
|
try {
|
|
|
|
if (powerInfo) {
|
|
|
|
powerInfo.balancesList.forEach((a) => {
|
|
|
|
balance = a.balance;
|
|
|
|
address = a.addr.addr;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
Logs.error(`Powergate powerInfo does not exist.`);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
}
|
|
|
|
|
|
|
|
let storageDeals = [];
|
|
|
|
try {
|
2020-11-22 04:23:56 +03:00
|
|
|
const listStorageResult = await power.storageDealRecords({
|
2020-09-16 11:40:03 +03:00
|
|
|
ascending: false,
|
|
|
|
includePending: false,
|
|
|
|
includeFinal: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
listStorageResult.recordsList.forEach((o) => {
|
|
|
|
storageDeals.push({
|
|
|
|
dealId: o.dealInfo.dealId,
|
|
|
|
rootCid: o.rootCid,
|
|
|
|
proposalCid: o.dealInfo.proposalCid,
|
|
|
|
pieceCid: o.dealInfo.pieceCid,
|
|
|
|
addr: o.addr,
|
|
|
|
size: o.dealInfo.size,
|
|
|
|
// NOTE(jim): formatted size.
|
|
|
|
formattedSize: Strings.bytesToSize(o.dealInfo.size),
|
|
|
|
pricePerEpoch: o.dealInfo.pricePerEpoch,
|
|
|
|
startEpoch: o.dealInfo.startEpoch,
|
|
|
|
// NOTE(jim): just for point of reference on the total cost.
|
2020-10-19 08:08:02 +03:00
|
|
|
totalCostFIL: Strings.formatAsFilecoinConversion(
|
2020-09-16 11:40:03 +03:00
|
|
|
o.dealInfo.pricePerEpoch * o.dealInfo.duration
|
|
|
|
),
|
2020-10-19 08:08:02 +03:00
|
|
|
totalCostAttoFIL: o.dealInfo.pricePerEpoch * o.dealInfo.duration,
|
2020-09-16 11:40:03 +03:00
|
|
|
duration: o.dealInfo.duration,
|
2020-10-19 02:26:14 +03:00
|
|
|
formattedDuration: Strings.getDaysFromEpoch(o.dealInfo.duration),
|
2020-09-16 11:40:03 +03:00
|
|
|
activationEpoch: o.dealInfo.activationEpoch,
|
|
|
|
time: o.time,
|
|
|
|
pending: o.pending,
|
2020-10-19 02:26:14 +03:00
|
|
|
createdAt: Strings.toDateSinceEpoch(o.time),
|
2020-10-19 08:08:02 +03:00
|
|
|
userEncryptsDeals: !!user.data.allow_encrypted_data_storage,
|
2020-10-21 02:59:38 +03:00
|
|
|
miner: minerMap[o.dealInfo.miner] ? minerMap[o.dealInfo.miner] : { id: o.dealInfo.miner },
|
2020-10-19 02:26:14 +03:00
|
|
|
user: {
|
|
|
|
id: user.id,
|
|
|
|
username: user.username,
|
|
|
|
photo: user.data.photo,
|
2020-10-19 08:08:02 +03:00
|
|
|
name: user.data.name,
|
2020-10-19 02:26:14 +03:00
|
|
|
},
|
2020-09-16 11:40:03 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
}
|
|
|
|
|
|
|
|
printData.address = address;
|
|
|
|
printData.balanceAttoFil = balance;
|
|
|
|
|
|
|
|
Logs.taskTimeless(`\x1b[36m\x1b[1mhttps://slate.host/${user.username}\x1b[0m`);
|
|
|
|
Logs.taskTimeless(`\x1b[36m\x1b[1m${address}\x1b[0m`);
|
|
|
|
Logs.taskTimeless(`\x1b[36m\x1b[1m${Strings.bytesToSize(userBytes)} stored each deal.\x1b[0m`);
|
|
|
|
Logs.taskTimeless(
|
|
|
|
`\x1b[36m\x1b[1m${Strings.formatAsFilecoinConversion(balance)} remaining\x1b[0m`
|
|
|
|
);
|
|
|
|
|
2020-10-19 02:26:14 +03:00
|
|
|
// NOTE(jim): Anyone can get a list for storage deals from Slate.
|
|
|
|
if (WRITE_TO_SLATE_STORAGE_DEAL_INDEX) {
|
|
|
|
const hasDealId = (id) => db.raw(`?? @> ?::jsonb`, ["data", JSON.stringify({ dealId: id })]);
|
|
|
|
|
|
|
|
for (let d = 0; d < storageDeals.length; d++) {
|
|
|
|
const dealToSave = storageDeals[d];
|
|
|
|
Logs.note(`Saving ${dealToSave.dealId} ...`);
|
|
|
|
|
|
|
|
console.log(dealToSave);
|
2020-11-22 04:23:56 +03:00
|
|
|
const existing = await db
|
|
|
|
.select("*")
|
|
|
|
.from("deals")
|
|
|
|
.where(hasDealId(dealToSave.dealId));
|
2020-10-19 02:26:14 +03:00
|
|
|
console.log(existing);
|
|
|
|
|
|
|
|
if (existing && !existing.error && existing.length) {
|
|
|
|
Logs.error(`${dealToSave.dealId} is already saved.`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
Logs.note(`Inserting ${dealToSave.dealId} ...`);
|
2020-10-19 08:08:02 +03:00
|
|
|
await delay(1000);
|
2020-11-22 04:23:56 +03:00
|
|
|
await db
|
|
|
|
.insert({ data: dealToSave, owner_user_id: user.id })
|
|
|
|
.into("deals")
|
|
|
|
.returning("*");
|
2020-10-19 02:26:14 +03:00
|
|
|
Logs.task(`Inserted ${dealToSave.dealId} !!!`);
|
|
|
|
}
|
|
|
|
}
|
2020-09-16 11:40:03 +03:00
|
|
|
|
2020-10-21 02:59:38 +03:00
|
|
|
// NOTE(jim): Exit early for analytics purposes.
|
|
|
|
if (STORE_MEANINGFUL_ADDRESS_ONLY_AND_PERFORM_NO_ACTIONS) {
|
|
|
|
Logs.taskTimeless(`Adding address for: ${user.username}`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-10-09 00:24:25 +03:00
|
|
|
// NOTE(jim): Skip users that are out of funds.
|
|
|
|
if (balance === 0) {
|
|
|
|
Logs.error(`OUT OF FUNDS: ${user.username}`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-09-16 11:40:03 +03:00
|
|
|
// NOTE(jim): tracks all buckets.
|
|
|
|
printData.buckets = [];
|
|
|
|
|
|
|
|
for (let j = 0; j < userBuckets.length; j++) {
|
|
|
|
const keyBucket = userBuckets[j];
|
|
|
|
let key;
|
|
|
|
let encrypt;
|
|
|
|
|
|
|
|
if (keyBucket.name.startsWith("open-")) {
|
|
|
|
Logs.note(`bucket found: open-data ${keyBucket.key}`);
|
2020-10-19 08:08:02 +03:00
|
|
|
Logs.note(`checking size ...`);
|
|
|
|
|
|
|
|
let bucketSizeBytes = null;
|
|
|
|
try {
|
|
|
|
const path = await buckets.listPath(keyBucket.key, "/");
|
|
|
|
bucketSizeBytes = path.item.size;
|
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bucketSizeBytes && bucketSizeBytes < MINIMUM_BYTES_FOR_STORAGE) {
|
|
|
|
try {
|
|
|
|
Logs.error(`we must kill this bucket ...`);
|
|
|
|
await buckets.remove(keyBucket.key);
|
|
|
|
Logs.note(`bucket removed ...`);
|
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bucketSizeBytes && bucketSizeBytes >= MINIMUM_BYTES_FOR_STORAGE) {
|
|
|
|
Logs.task(`bucket is okay !!!`);
|
|
|
|
key = keyBucket.key;
|
|
|
|
}
|
2020-09-16 11:40:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (keyBucket.name.startsWith("encrypted-data-")) {
|
|
|
|
Logs.note(`bucket found: encrypted-data ${keyBucket.key}`);
|
2020-10-19 08:08:02 +03:00
|
|
|
Logs.note(`checking size ...`);
|
|
|
|
|
|
|
|
let bucketSizeBytes = null;
|
|
|
|
try {
|
|
|
|
const path = await buckets.listPath(keyBucket.key, "/");
|
|
|
|
bucketSizeBytes = path.item.size;
|
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bucketSizeBytes && bucketSizeBytes < MINIMUM_BYTES_FOR_STORAGE) {
|
|
|
|
try {
|
|
|
|
Logs.error(`we must kill this bucket ...`);
|
|
|
|
await buckets.remove(keyBucket.key);
|
|
|
|
Logs.note(`bucket removed ...`);
|
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bucketSizeBytes && bucketSizeBytes >= MINIMUM_BYTES_FOR_STORAGE) {
|
|
|
|
Logs.task(`bucket is okay !!!`);
|
|
|
|
key = keyBucket.key;
|
|
|
|
}
|
2020-09-16 11:40:03 +03:00
|
|
|
}
|
|
|
|
|
2020-10-19 20:03:22 +03:00
|
|
|
// NOTE(jim): Temporarily prevent more buckets from being created for legacy accounts.
|
|
|
|
if (
|
|
|
|
keyBucket.name === "data" &&
|
|
|
|
!SKIP_NEW_BUCKET_CREATION &&
|
|
|
|
userBuckets.length < BUCKET_LIMIT
|
|
|
|
) {
|
2020-09-16 11:40:03 +03:00
|
|
|
key = null;
|
|
|
|
encrypt = !!user.data.allow_encrypted_data_storage;
|
|
|
|
|
|
|
|
// NOTE(jim): Create a new bucket
|
|
|
|
const newBucketName = encrypt ? `encrypted-data-${uuid()}` : `open-data-${uuid()}`;
|
|
|
|
|
|
|
|
// NOTE(jim): Get the root key of the bucket
|
2020-10-19 08:08:02 +03:00
|
|
|
let bucketSizeBytes = null;
|
2020-09-16 11:40:03 +03:00
|
|
|
let items;
|
|
|
|
try {
|
|
|
|
const path = await buckets.listPath(keyBucket.key, "/");
|
|
|
|
items = path.item;
|
2020-10-19 08:08:02 +03:00
|
|
|
bucketSizeBytes = path.item.size;
|
2020-09-16 11:40:03 +03:00
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
}
|
|
|
|
|
2020-10-19 08:08:02 +03:00
|
|
|
if (bucketSizeBytes && bucketSizeBytes < MINIMUM_BYTES_FOR_STORAGE) {
|
|
|
|
Logs.error(`Root 'data' bucket does not fit size requirements. Skipping.`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
await delay(1000);
|
|
|
|
|
2020-09-16 11:40:03 +03:00
|
|
|
Logs.task(`creating new bucket: ${newBucketName}.`);
|
|
|
|
|
|
|
|
// NOTE(jim): Create a new bucket
|
|
|
|
try {
|
2020-10-19 20:03:22 +03:00
|
|
|
Logs.note(`attempting ... `);
|
2020-09-16 11:40:03 +03:00
|
|
|
|
|
|
|
if (!PRACTICE_RUN) {
|
2020-10-19 20:03:22 +03:00
|
|
|
Logs.note(`name: ${newBucketName} ...`);
|
|
|
|
Logs.note(`cid: ${items.cid} ...`);
|
2020-09-16 11:40:03 +03:00
|
|
|
let newBucket = await buckets.create(newBucketName, encrypt, items.cid);
|
|
|
|
|
|
|
|
key = newBucket.root.key;
|
|
|
|
|
2020-10-19 20:03:22 +03:00
|
|
|
Logs.task(`created ${newBucketName} successfully with new key ${key}.`);
|
2020-09-16 11:40:03 +03:00
|
|
|
} else {
|
|
|
|
Logs.note(`practice skipping ...`);
|
2020-10-19 20:03:22 +03:00
|
|
|
continue;
|
2020-09-16 11:40:03 +03:00
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
}
|
2020-10-19 08:08:02 +03:00
|
|
|
|
|
|
|
await delay(5000);
|
2020-09-16 11:40:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (key) {
|
2020-10-19 02:26:14 +03:00
|
|
|
await delay(500);
|
2020-10-18 21:10:42 +03:00
|
|
|
|
2020-09-16 11:40:03 +03:00
|
|
|
try {
|
|
|
|
if (!PRACTICE_RUN) {
|
|
|
|
await buckets.archive(key);
|
|
|
|
Logs.task(`\x1b[32mNEW DEAL SUCCESSFUL !!!\x1b[0m`);
|
|
|
|
} else {
|
|
|
|
Logs.note(`archive skipping ...`);
|
|
|
|
}
|
|
|
|
|
|
|
|
printData.buckets.push({
|
|
|
|
key,
|
|
|
|
success: false,
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
if (e.message === `the same bucket cid is already archived successfully`) {
|
|
|
|
printData.buckets.push({
|
|
|
|
key,
|
|
|
|
success: true,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
printData.buckets.push({
|
|
|
|
key,
|
|
|
|
success: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Logs.note(e.message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let k = 0; k < printData.buckets.length; k++) {
|
|
|
|
let targetBucket = printData.buckets[k];
|
|
|
|
|
|
|
|
if (targetBucket.success) {
|
|
|
|
try {
|
|
|
|
Logs.task(`deleting bucket with key: ${targetBucket.key}`);
|
|
|
|
await buckets.remove(targetBucket.key);
|
|
|
|
Logs.task(`successfully deleted ${targetBucket.key}`);
|
|
|
|
} catch (e) {
|
|
|
|
Logs.error(e.message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log("\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
Logs.task(`total storage per run: ${Strings.bytesToSize(bytes)}`);
|
|
|
|
Logs.task(`total storage per run (with replication x5): ${Strings.bytesToSize(bytes * 5)}`);
|
|
|
|
Logs.task(`creating slate-storage-addresses.json`);
|
|
|
|
|
2020-10-18 21:10:42 +03:00
|
|
|
console.log(`${STORAGE_BOT_NAME} finished. \n\n`);
|
2020-09-16 11:40:03 +03:00
|
|
|
console.log(`FINISHED: worker-heavy-stones.js`);
|
|
|
|
console.log(` CTRL +C to return to terminal.`);
|
|
|
|
};
|
|
|
|
|
|
|
|
run();
|