mirror of
https://github.com/filecoin-project/slate.git
synced 2024-11-22 03:56:49 +03:00
removed storage deal options and made edits to storage deal script
This commit is contained in:
parent
b2ddaf183c
commit
c3f2d7c617
@ -134,14 +134,6 @@ export const sendTemplateEmail = async (data) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const archive = async (data) => {
|
||||
await Websockets.checkWebsocket();
|
||||
return await returnJSON(`/api/data/archive`, {
|
||||
...DEFAULT_OPTIONS,
|
||||
body: JSON.stringify({ data }),
|
||||
});
|
||||
};
|
||||
|
||||
export const removeFromBucket = async (data) => {
|
||||
await Websockets.checkWebsocket();
|
||||
return await returnJSON(`/api/data/bucket-remove`, {
|
||||
|
@ -172,18 +172,6 @@ export const navigation = [
|
||||
pathname: "/_/directory",
|
||||
},
|
||||
slatePage,
|
||||
{
|
||||
id: "NAV_FILECOIN",
|
||||
name: "Filecoin",
|
||||
pageTitle: "Archive on Filecoin",
|
||||
pathname: "/_/filecoin",
|
||||
},
|
||||
{
|
||||
id: "NAV_STORAGE_DEAL",
|
||||
name: "Storage Deal",
|
||||
pageTitle: "Filecoin Storage Deal",
|
||||
pathname: "/_/storage-deal",
|
||||
},
|
||||
{
|
||||
id: "NAV_API",
|
||||
name: "API",
|
||||
|
@ -27,15 +27,12 @@ import SceneSlate from "~/scenes/SceneSlate";
|
||||
import SceneActivity from "~/scenes/SceneActivity";
|
||||
import SceneDirectory from "~/scenes/SceneDirectory";
|
||||
import SceneProfile from "~/scenes/SceneProfile";
|
||||
import SceneArchive from "~/scenes/SceneArchive";
|
||||
import SceneMakeFilecoinDeal from "~/scenes/SceneMakeFilecoinDeal";
|
||||
|
||||
// NOTE(jim):
|
||||
// Sidebars each have a decorator and can be shown to with _handleAction
|
||||
import SidebarCreateSlate from "~/components/sidebars/SidebarCreateSlate";
|
||||
import SidebarCreateWalletAddress from "~/components/sidebars/SidebarCreateWalletAddress";
|
||||
import SidebarWalletSendFunds from "~/components/sidebars/SidebarWalletSendFunds";
|
||||
import SidebarFileStorageDeal from "~/components/sidebars/SidebarFileStorageDeal";
|
||||
import SidebarAddFileToSlate from "~/components/sidebars/SidebarAddFileToSlate";
|
||||
import SidebarDragDropNotice from "~/components/sidebars/SidebarDragDropNotice";
|
||||
import SidebarSingleSlateSettings from "~/components/sidebars/SidebarSingleSlateSettings";
|
||||
@ -62,7 +59,6 @@ import { LoaderSpinner } from "~/components/system/components/Loaders";
|
||||
|
||||
const SIDEBARS = {
|
||||
SIDEBAR_FILECOIN_ARCHIVE: <SidebarFilecoinArchive />,
|
||||
SIDEBAR_FILE_STORAGE_DEAL: <SidebarFileStorageDeal />,
|
||||
SIDEBAR_WALLET_SEND_FUNDS: <SidebarWalletSendFunds />,
|
||||
SIDEBAR_CREATE_WALLET_ADDRESS: <SidebarCreateWalletAddress />,
|
||||
SIDEBAR_ADD_FILE_TO_SLATE: <SidebarAddFileToSlate />,
|
||||
@ -86,8 +82,6 @@ const SCENES = {
|
||||
NAV_API: <SceneSettingsDeveloper />,
|
||||
NAV_SETTINGS: <SceneEditAccount />,
|
||||
NAV_SLATES: <SceneSlates />,
|
||||
NAV_FILECOIN: <SceneArchive />,
|
||||
NAV_STORAGE_DEAL: <SceneMakeFilecoinDeal />,
|
||||
};
|
||||
|
||||
let mounted;
|
||||
|
@ -196,24 +196,6 @@ export class ApplicationUserControlsPopup extends React.Component {
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
text: (
|
||||
<div css={STYLES_SECTION_ITEM_HOVER}>
|
||||
<Link href={"/_/filecoin"} onAction={this._handleAction}>
|
||||
Filecoin
|
||||
</Link>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<div css={STYLES_SECTION_ITEM_HOVER}>
|
||||
<Link href={"/_/storage-deal"} onAction={this._handleAction}>
|
||||
Storage deal
|
||||
</Link>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<div css={STYLES_SECTION_ITEM_HOVER}>
|
||||
|
@ -1,164 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as Strings from "~/common/strings";
|
||||
import * as Constants from "~/common/constants";
|
||||
import * as System from "~/components/system";
|
||||
|
||||
import { css } from "@emotion/react";
|
||||
|
||||
const STYLES_FOCUS = css`
|
||||
font-size: ${Constants.typescale.lvl1};
|
||||
font-family: ${Constants.font.medium};
|
||||
overflow-wrap: break-word;
|
||||
width: 100%;
|
||||
|
||||
strong {
|
||||
font-family: ${Constants.font.semiBold};
|
||||
font-weight: 400;
|
||||
}
|
||||
`;
|
||||
|
||||
const STYLES_SUBTEXT = css`
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
const STYLES_ITEM = css`
|
||||
margin-top: 16px;
|
||||
`;
|
||||
|
||||
const STYLES_IMAGE_PREVIEW = css`
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 48px;
|
||||
`;
|
||||
|
||||
export default class SidebarFileStorageDeal extends React.Component {
|
||||
state = {
|
||||
settings_cold_default_duration: this.props.viewer.settings_cold_default_duration,
|
||||
settings_cold_default_replication_factor: this.props.viewer
|
||||
.settings_cold_default_replication_factor,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.props.viewer.settingsDealsAutoApprove) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this._handleSubmit();
|
||||
}
|
||||
|
||||
_handleMakeDeal = async ({ ipfs }) => {
|
||||
const options = {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ ipfs }),
|
||||
};
|
||||
|
||||
const response = await fetch("/api/data/storage-deal", options);
|
||||
const json = await response.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
_handleSubmit = async (e) => {
|
||||
if (e) {
|
||||
e.persist();
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
await this._handleMakeDeal({ ipfs: `/ipfs/${this.props.data.cid}` });
|
||||
this.setState({ loading: false });
|
||||
await this.props.onCancel();
|
||||
};
|
||||
|
||||
_handleCancel = () => {
|
||||
this.props.onCancel();
|
||||
};
|
||||
|
||||
_handleChange = (e) => {
|
||||
this.setState({ [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const file = this.props.data;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<System.P1
|
||||
style={{
|
||||
fontFamily: Constants.font.semiBold,
|
||||
fontSize: Constants.typescale.lvl3,
|
||||
}}
|
||||
>
|
||||
Make Filecoin storage deal
|
||||
</System.P1>
|
||||
|
||||
<div>
|
||||
<div css={STYLES_ITEM}>
|
||||
<div css={STYLES_FOCUS}>{file.name}</div>
|
||||
<div css={STYLES_SUBTEXT}>Name</div>
|
||||
</div>
|
||||
|
||||
<div css={STYLES_ITEM}>
|
||||
<div css={STYLES_FOCUS}>{Strings.bytesToSize(file.size)}</div>
|
||||
<div css={STYLES_SUBTEXT}>File size</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!this.state.loading ? (
|
||||
<System.Input
|
||||
containerStyle={{ marginTop: 48 }}
|
||||
label="Deal duration"
|
||||
name="settings_cold_default_duration"
|
||||
placeholder="Type in epochs (~25 seconds)"
|
||||
type="number"
|
||||
value={this.state.settings_cold_default_duration}
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!this.state.loading ? (
|
||||
<System.Input
|
||||
containerStyle={{ marginTop: 24 }}
|
||||
label="Replication factor"
|
||||
name="settings_cold_default_replication_factor"
|
||||
value={this.state.settings_cold_default_replication_factor}
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!this.state.loading ? (
|
||||
<System.SelectMenu
|
||||
full
|
||||
containerStyle={{ marginTop: 24 }}
|
||||
name="address"
|
||||
label="Payment address"
|
||||
value={this.props.selected.address}
|
||||
category="address"
|
||||
onChange={this.props.onSelectedChange}
|
||||
options={this.props.viewer.addresses}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<System.ButtonPrimary
|
||||
full
|
||||
style={{ marginTop: 48 }}
|
||||
onClick={this._handleSubmit}
|
||||
loading={this.state.loading}
|
||||
>
|
||||
Make storage deal
|
||||
</System.ButtonPrimary>
|
||||
|
||||
{!this.state.loading ? (
|
||||
<System.ButtonSecondary full style={{ marginTop: 16 }} onClick={this._handleCancel}>
|
||||
Cancel deal
|
||||
</System.ButtonSecondary>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
@ -22,9 +22,6 @@
|
||||
// textileToken: user.textileToken,
|
||||
// textileThreadID: user.textileThreadID,
|
||||
// textileBucketCID: user.textileThreadID,
|
||||
// settingsDealAutoApprove: user.settingsDealAutoApprove,
|
||||
// allowAutomaticDataStorage: user.allowAutomaticDataStorage,
|
||||
// allowEncryptedDataStorage: user.allowEncryptedDataStorage,
|
||||
// onboarding: user.onboarding,
|
||||
// };
|
||||
// };
|
||||
|
@ -1,206 +0,0 @@
|
||||
import * as Constants from "~/node_common/constants";
|
||||
import * as Utilities from "~/node_common/utilities";
|
||||
import * as Social from "~/node_common/social";
|
||||
import * as Strings from "~/common/strings";
|
||||
import * as Logging from "~/common/logging";
|
||||
import * as RequestUtilities from "~/node_common/request-utilities";
|
||||
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { MAX_BUCKET_COUNT, MIN_ARCHIVE_SIZE_BYTES } from "~/node_common/constants";
|
||||
|
||||
export default async (req, res) => {
|
||||
const userInfo = await RequestUtilities.checkAuthorizationInternal(req, res);
|
||||
if (!userInfo) return;
|
||||
const { id, user } = userInfo;
|
||||
|
||||
let bucketName = Constants.textile.dealsBucket;
|
||||
if (req.body.data && req.body.data.bucketName) {
|
||||
bucketName = req.body.data.bucketName;
|
||||
}
|
||||
|
||||
const { buckets, bucketKey } = await Utilities.getBucket({
|
||||
user,
|
||||
bucketName,
|
||||
});
|
||||
|
||||
if (!buckets) {
|
||||
return res.status(500).send({
|
||||
decorator: "SERVER_NO_BUCKET_DATA",
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE(jim): Getting the appropriate bucket key
|
||||
|
||||
let items = null;
|
||||
let bucketSizeBytes = 0;
|
||||
try {
|
||||
const path = await buckets.listPath(bucketKey, "/");
|
||||
items = path.item;
|
||||
bucketSizeBytes = path.item.size;
|
||||
} catch (e) {
|
||||
Social.sendTextileSlackMessage({
|
||||
file: "/pages/api/data/archive.js",
|
||||
user,
|
||||
message: e.message,
|
||||
code: e.code,
|
||||
functionName: `buckets.listPath`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!items) {
|
||||
return res.status(500).send({
|
||||
decorator: "SERVER_NO_BUCKET_DATA",
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
|
||||
Logging.log(`[ deal ] will make a deal for ${items.items.length} items`);
|
||||
if (items.items.length < 2) {
|
||||
return res.status(500).send({
|
||||
decorator: "SERVER_ARCHIVE_NO_FILES",
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
|
||||
Logging.log(`[ deal ] deal size: ${Strings.bytesToSize(bucketSizeBytes)}`);
|
||||
if (bucketSizeBytes < MIN_ARCHIVE_SIZE_BYTES) {
|
||||
return res.status(500).send({
|
||||
decorator: "SERVER_ARCHIVE_BUCKET_TOO_SMALL",
|
||||
message: `Your deal size of ${Strings.bytesToSize(
|
||||
bucketSizeBytes
|
||||
)} is too small. You must provide at least 100MB.`,
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE(jim): Make sure that you haven't hit the MAX_BUCKET_COUNT
|
||||
|
||||
let userBuckets = [];
|
||||
try {
|
||||
userBuckets = await buckets.list();
|
||||
} catch (e) {
|
||||
Social.sendTextileSlackMessage({
|
||||
file: "/pages/api/data/archive.js",
|
||||
user: user,
|
||||
message: e.message,
|
||||
code: e.code,
|
||||
functionName: `buckets.list`,
|
||||
});
|
||||
|
||||
return res.status(500).send({
|
||||
decorator: "SERVER_ARCHIVE_BUCKET_COUNT_VERIFICATION_FAILED",
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
|
||||
Logging.log(
|
||||
`[ encrypted ] user has ${userBuckets.length} out of ${MAX_BUCKET_COUNT} buckets used.`
|
||||
);
|
||||
if (userBuckets.length >= MAX_BUCKET_COUNT) {
|
||||
return res.status(500).send({
|
||||
decorator: "SERVER_ARCHIVE_MAX_NUMBER_BUCKETS",
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE(jim): Either encrypt the bucket or don't encrypt the bucket.
|
||||
let encryptThisDeal = false;
|
||||
if (bucketName !== Constants.textile.dealsBucket && user.allowEncryptedDataStorage) {
|
||||
encryptThisDeal = true;
|
||||
}
|
||||
|
||||
if (req.body.data.forceEncryption) {
|
||||
encryptThisDeal = true;
|
||||
}
|
||||
|
||||
let key = bucketKey;
|
||||
let encryptedBucketName = null;
|
||||
if (user.allowEncryptedDataStorage || req.body.data.forceEncryption) {
|
||||
encryptedBucketName = req.body.data.forceEncryption
|
||||
? `encrypted-deal-${uuid()}`
|
||||
: `encrypted-data-${uuid()}`;
|
||||
|
||||
Logging.log(`[ encrypted ] making an ${encryptedBucketName} for this storage deal.`);
|
||||
|
||||
try {
|
||||
const newBucket = await buckets.create(encryptedBucketName, true, items.cid);
|
||||
key = newBucket.root.key;
|
||||
} catch (e) {
|
||||
Social.sendTextileSlackMessage({
|
||||
file: "/pages/api/data/archive.js",
|
||||
user: user,
|
||||
message: e.message,
|
||||
code: e.code,
|
||||
functionName: `buckets.create (encrypted)`,
|
||||
});
|
||||
|
||||
return res.status(500).send({
|
||||
decorator: "SERVER_ARCHIVE_ENCRYPTION_FAILED",
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
|
||||
Logging.log(`[ encrypted ] ${encryptedBucketName}`);
|
||||
Logging.log(`[ encrypted ] ${key}`);
|
||||
} else {
|
||||
const newDealBucketName = `open-deal-${uuid()}`;
|
||||
|
||||
try {
|
||||
const newBucket = await buckets.create(newDealBucketName, false, items.cid);
|
||||
key = newBucket.root.key;
|
||||
} catch (e) {
|
||||
Social.sendTextileSlackMessage({
|
||||
file: "/pages/api/data/archive.js",
|
||||
user: user,
|
||||
message: e.message,
|
||||
code: e.code,
|
||||
functionName: `buckets.create (normal, not encrypted)`,
|
||||
});
|
||||
|
||||
return res.status(500).send({
|
||||
decorator: "SERVER_ARCHIVE_BUCKET_CLONING_FAILED",
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
|
||||
Logging.log(`[ normal ] ${newDealBucketName}`);
|
||||
Logging.log(`[ normal ] ${key}`);
|
||||
}
|
||||
|
||||
// NOTE(jim): Finally make the deal
|
||||
|
||||
let response = {};
|
||||
let error = {};
|
||||
try {
|
||||
Logging.log(`[ deal-maker ] deal being made for ${key}`);
|
||||
if (req.body.data && req.body.data.settings) {
|
||||
response = await buckets.archive(key, req.body.data.settings);
|
||||
} else {
|
||||
response = await buckets.archive(key);
|
||||
}
|
||||
} catch (e) {
|
||||
error.message = e.message;
|
||||
error.code = e.code;
|
||||
Logging.log(e.message);
|
||||
|
||||
Social.sendTextileSlackMessage({
|
||||
file: "/pages/api/data/archive.js",
|
||||
user: user,
|
||||
message: e.message,
|
||||
code: e.code,
|
||||
functionName: `buckets.archive`,
|
||||
});
|
||||
|
||||
return res.status(500).send({
|
||||
decorator: "SERVER_ARCHIVE_DEAL_FAILED",
|
||||
error: true,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
decorator: "SERVER_ARCHIVE",
|
||||
data: { response, error },
|
||||
});
|
||||
};
|
@ -1,200 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as System from "~/components/system";
|
||||
import * as Constants from "~/common/constants";
|
||||
import * as Actions from "~/common/actions";
|
||||
import * as Strings from "~/common/strings";
|
||||
|
||||
import { css } from "@emotion/react";
|
||||
import { LoaderSpinner } from "~/components/system/components/Loaders";
|
||||
import { SecondaryTabGroup } from "~/components/core/TabGroup";
|
||||
|
||||
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
|
||||
import ScenePage from "~/components/core/ScenePage";
|
||||
import ScenePageHeader from "~/components/core/ScenePageHeader";
|
||||
import SceneSettings from "~/scenes/SceneSettings";
|
||||
import SceneDeals from "~/scenes/SceneDeals";
|
||||
import SceneWallet from "~/scenes/SceneWallet";
|
||||
|
||||
const STYLES_SPINNER_CONTAINER = css`
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
let mounted = false;
|
||||
|
||||
export default class SceneArchive extends React.Component {
|
||||
state = {
|
||||
deals: [],
|
||||
dealsLoaded: false,
|
||||
networkViewer: null,
|
||||
allowAutomaticDataStorage: this.props.viewer.allowAutomaticDataStorage,
|
||||
allowEncryptedDataStorage: this.props.viewer.allowEncryptedDataStorage,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
if (mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
mounted = true;
|
||||
let networkViewer;
|
||||
try {
|
||||
const response = await fetch("/api/network");
|
||||
const json = await response.json();
|
||||
networkViewer = json.data;
|
||||
} catch (e) {}
|
||||
|
||||
this.setState({
|
||||
networkViewer,
|
||||
});
|
||||
|
||||
let deals = [];
|
||||
try {
|
||||
const response = await fetch("/api/network-deals");
|
||||
const json = await response.json();
|
||||
deals = json.data.deals;
|
||||
} catch (e) {}
|
||||
|
||||
if (!deals || !deals.length) {
|
||||
this.setState({ dealsLoaded: true });
|
||||
}
|
||||
|
||||
this.setState({ deals, dealsLoaded: true });
|
||||
}
|
||||
|
||||
_handleCheckboxChange = (e) => {
|
||||
this.setState({ [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
_handleSaveFilecoin = async (e) => {
|
||||
this.setState({ changingFilecoin: true });
|
||||
|
||||
await Actions.updateViewer({
|
||||
user: {
|
||||
allowAutomaticDataStorage: this.state.allowAutomaticDataStorage,
|
||||
allowEncryptedDataStorage: this.state.allowEncryptedDataStorage,
|
||||
},
|
||||
});
|
||||
|
||||
this.setState({ changingFilecoin: false });
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
mounted = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
let tab = this.props.page.params?.tab || "archive";
|
||||
return (
|
||||
<WebsitePrototypeWrapper
|
||||
title={`${this.props.page.pageTitle} • Slate`}
|
||||
url={`${Constants.hostname}${this.props.page.pathname}`}
|
||||
>
|
||||
<ScenePage>
|
||||
<ScenePageHeader title="Filecoin">
|
||||
{/* Use this section to archive all of your data on to Filecoin through a storage deal. You
|
||||
must have at last 100MB stored to make an archive storage deal. */}
|
||||
</ScenePageHeader>
|
||||
|
||||
<SecondaryTabGroup
|
||||
tabs={[
|
||||
{ title: "Archive Settings", value: { tab: "archive" } },
|
||||
{ title: "Wallet", value: { tab: "wallet" } },
|
||||
]}
|
||||
value={tab}
|
||||
onAction={this.props.onAction}
|
||||
/>
|
||||
|
||||
{this.state.networkViewer ? (
|
||||
<React.Fragment>
|
||||
{tab === "archive" ? (
|
||||
<React.Fragment>
|
||||
<ScenePageHeader>
|
||||
Use this section to archive all of your data on to Filecoin through a storage
|
||||
deal. You must have at last 100MB stored to make an archive storage deal.
|
||||
</ScenePageHeader>
|
||||
|
||||
<System.P1 style={{ marginTop: 24 }}>
|
||||
Archive all of your data onto the Filecoin Network with a storage deal using
|
||||
your default settings.
|
||||
</System.P1>
|
||||
<br />
|
||||
<System.ButtonPrimary
|
||||
onClick={() =>
|
||||
this.props.onAction({
|
||||
type: "SIDEBAR",
|
||||
value: "SIDEBAR_FILECOIN_ARCHIVE",
|
||||
})
|
||||
}
|
||||
>
|
||||
Archive your data
|
||||
</System.ButtonPrimary>
|
||||
|
||||
<System.DescriptionGroup
|
||||
style={{ marginTop: 64 }}
|
||||
label="Archive automation settings"
|
||||
description="Configure the automation settings for your archive storage deals."
|
||||
/>
|
||||
|
||||
<System.CheckBox
|
||||
style={{ marginTop: 24 }}
|
||||
name="allowAutomaticDataStorage"
|
||||
value={this.state.allowAutomaticDataStorage}
|
||||
onChange={this._handleCheckboxChange}
|
||||
>
|
||||
Allow Slate to make archive storage deals on your behalf to the Filecoin
|
||||
Network. You will get a receipt in the Filecoin section.
|
||||
</System.CheckBox>
|
||||
|
||||
<System.CheckBox
|
||||
style={{ marginTop: 24 }}
|
||||
name="allowEncryptedDataStorage"
|
||||
value={this.state.allowEncryptedDataStorage}
|
||||
onChange={this._handleCheckboxChange}
|
||||
>
|
||||
Force encryption on archive storage deals (only you can see retrieved data from
|
||||
the Filecoin network).
|
||||
</System.CheckBox>
|
||||
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<System.ButtonPrimary
|
||||
onClick={this._handleSaveFilecoin}
|
||||
loading={this.state.changingFilecoin}
|
||||
>
|
||||
Save archiving settings
|
||||
</System.ButtonPrimary>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<SceneSettings {...this.props} networkViewer={this.state.networkViewer} />
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
|
||||
{tab === "wallet" ? (
|
||||
<React.Fragment>
|
||||
<SceneWallet {...this.props} networkViewer={this.state.networkViewer} />
|
||||
<br />
|
||||
<br />
|
||||
{this.state.dealsLoaded ? (
|
||||
<SceneDeals deals={this.state.deals} dealsLoaded={this.state.dealsLoaded} />
|
||||
) : (
|
||||
<div css={STYLES_SPINNER_CONTAINER}>
|
||||
<LoaderSpinner style={{ height: 32, width: 32 }} />
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<div css={STYLES_SPINNER_CONTAINER}>
|
||||
<LoaderSpinner style={{ height: 32, width: 32 }} />
|
||||
</div>
|
||||
)}
|
||||
</ScenePage>
|
||||
</WebsitePrototypeWrapper>
|
||||
);
|
||||
}
|
||||
}
|
@ -50,8 +50,6 @@ export default class SceneEditAccount extends React.Component {
|
||||
photo: this.props.viewer.photo,
|
||||
name: this.props.viewer.name,
|
||||
deleting: false,
|
||||
allowAutomaticDataStorage: this.props.viewer.allowAutomaticDataStorage,
|
||||
allowEncryptedDataStorage: this.props.viewer.allowEncryptedDataStorage,
|
||||
changingPassword: false,
|
||||
changingAvatar: false,
|
||||
savingNameBio: false,
|
||||
@ -80,21 +78,6 @@ export default class SceneEditAccount extends React.Component {
|
||||
this.setState({ changingAvatar: false, photo: url });
|
||||
};
|
||||
|
||||
_handleSaveFilecoin = async (e) => {
|
||||
this.setState({ changingFilecoin: true });
|
||||
|
||||
let response = await Actions.updateViewer({
|
||||
user: {
|
||||
allowAutomaticDataStorage: this.state.allowAutomaticDataStorage,
|
||||
allowEncryptedDataStorage: this.state.allowEncryptedDataStorage,
|
||||
},
|
||||
});
|
||||
|
||||
Events.hasError(response);
|
||||
|
||||
this.setState({ changingFilecoin: false });
|
||||
};
|
||||
|
||||
_handleSave = async (e) => {
|
||||
if (!Validations.username(this.state.username)) {
|
||||
Events.dispatchMessage({
|
||||
@ -180,7 +163,6 @@ export default class SceneEditAccount extends React.Component {
|
||||
<SecondaryTabGroup
|
||||
tabs={[
|
||||
{ title: "Profile", value: { tab: "profile" } },
|
||||
{ title: "Data Storage", value: { tab: "storage" } },
|
||||
{ title: "Security", value: { tab: "security" } },
|
||||
{ title: "Account", value: { tab: "account" } },
|
||||
]}
|
||||
@ -239,48 +221,6 @@ export default class SceneEditAccount extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{tab === "storage" ? (
|
||||
<div style={{ maxWidth: 800 }}>
|
||||
<div css={STYLES_HEADER}>
|
||||
Allow Slate to make Filecoin archive storage deals on your behalf
|
||||
</div>
|
||||
<div style={{ maxWidth: 800 }}>
|
||||
If this box is checked, then we will make Filecoin archive storage deals on your
|
||||
behalf. By default these storage deals are not encrypted and anyone can retrieve
|
||||
them from the Filecoin Network.
|
||||
</div>
|
||||
|
||||
<System.CheckBox
|
||||
style={{ marginTop: 24 }}
|
||||
name="allowAutomaticDataStorage"
|
||||
value={this.state.allowAutomaticDataStorage}
|
||||
onChange={this._handleChange}
|
||||
>
|
||||
Allow Slate to make archive storage deals on your behalf to the Filecoin Network.
|
||||
You will get a receipt in the Filecoin section.
|
||||
</System.CheckBox>
|
||||
|
||||
<System.CheckBox
|
||||
style={{ marginTop: 24 }}
|
||||
name="allowEncryptedDataStorage"
|
||||
value={this.state.allowEncryptedDataStorage}
|
||||
onChange={this._handleChange}
|
||||
>
|
||||
Force encryption on archive storage deals (only you can see retrieved data from the
|
||||
Filecoin network).
|
||||
</System.CheckBox>
|
||||
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<System.ButtonPrimary
|
||||
onClick={this._handleSaveFilecoin}
|
||||
loading={this.state.changingFilecoin}
|
||||
style={{ width: "200px" }}
|
||||
>
|
||||
Save
|
||||
</System.ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{tab === "security" ? (
|
||||
<div>
|
||||
<div css={STYLES_HEADER}>Change password</div>
|
||||
|
@ -1,531 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as Strings from "~/common/strings";
|
||||
import * as Actions from "~/common/actions";
|
||||
import * as Constants from "~/common/constants";
|
||||
import * as System from "~/components/system";
|
||||
import * as SVG from "~/common/svg";
|
||||
import * as Window from "~/common/window";
|
||||
import * as Messages from "~/common/messages";
|
||||
import * as FileUtilities from "~/common/file-utilities";
|
||||
import * as Events from "~/common/custom-events";
|
||||
|
||||
import { css } from "@emotion/react";
|
||||
import { createState } from "~/scenes/SceneSettings";
|
||||
import { LoaderSpinner } from "~/components/system/components/Loaders";
|
||||
import { FilecoinNumber } from "@glif/filecoin-number";
|
||||
import { Link } from "~/components/core/Link";
|
||||
|
||||
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
|
||||
import Section from "~/components/core/Section";
|
||||
import ScenePage from "~/components/core/ScenePage";
|
||||
import ScenePageHeader from "~/components/core/ScenePageHeader";
|
||||
|
||||
const STYLES_SPINNER_CONTAINER = css`
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const STYLES_FILE_HIDDEN = css`
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
`;
|
||||
|
||||
const STYLES_ROW = css`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const STYLES_LEFT = css`
|
||||
color: ${Constants.system.black};
|
||||
transition: 200ms ease all;
|
||||
min-width: 10%;
|
||||
width: 100%;
|
||||
:visited {
|
||||
color: ${Constants.system.black};
|
||||
}
|
||||
`;
|
||||
|
||||
const STYLES_RIGHT = css`
|
||||
flex-shrink: 0;
|
||||
transition: 200ms ease all;
|
||||
cursor: pointer;
|
||||
:hover {
|
||||
color: ${Constants.system.blue};
|
||||
}
|
||||
`;
|
||||
|
||||
const DEFAULT_ERROR_MESSAGE = "We could not make your deal. Please try again later.";
|
||||
let mounted = false;
|
||||
|
||||
export default class SceneMakeFilecoinDeal extends React.Component {
|
||||
state = { encryption: false };
|
||||
|
||||
async componentDidMount() {
|
||||
if (mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
mounted = true;
|
||||
let networkViewer;
|
||||
try {
|
||||
const response = await fetch("/api/network");
|
||||
const json = await response.json();
|
||||
networkViewer = json.data;
|
||||
} catch (e) {}
|
||||
|
||||
if (networkViewer) {
|
||||
this.setState({
|
||||
networkViewer,
|
||||
...createState(networkViewer.settings),
|
||||
encryption: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_handleUpload = async (e) => {
|
||||
e.persist();
|
||||
|
||||
if (!e.target.files) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!e.target.files.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
|
||||
for (let i = 0; i < e.target.files.length; i++) {
|
||||
const file = e.target.files[i];
|
||||
|
||||
const response = await FileUtilities.upload({
|
||||
bucketName: Constants.textile.dealsBucket,
|
||||
file,
|
||||
});
|
||||
}
|
||||
|
||||
let networkViewer;
|
||||
try {
|
||||
const response = await fetch("/api/network");
|
||||
const json = await response.json();
|
||||
networkViewer = json.data;
|
||||
} catch (e) {}
|
||||
|
||||
this.setState({
|
||||
networkViewer,
|
||||
loading: false,
|
||||
});
|
||||
};
|
||||
|
||||
_handleArchive = async (e) => {
|
||||
this.setState({ archiving: true });
|
||||
|
||||
const response = await Actions.archive({
|
||||
bucketName: Constants.textile.dealsBucket,
|
||||
forceEncryption: this.state.encryption,
|
||||
settings: {
|
||||
/**
|
||||
* RepFactor indicates the desired amount of active deals
|
||||
* with different miners to store the data. While making deals
|
||||
* the other attributes of FilConfig are considered for miner selection.
|
||||
*/
|
||||
repFactor: Number(this.state.repFactor),
|
||||
|
||||
/**
|
||||
* DealMinDuration indicates the duration to be used when making new deals.
|
||||
*/
|
||||
dealMinDuration: this.state.dealMinDuration,
|
||||
|
||||
/**
|
||||
* ExcludedMiners is a set of miner addresses won't be ever be selected
|
||||
*when making new deals, even if they comply to other filters.
|
||||
*/
|
||||
excludedMiners: this.state.excludedMiners,
|
||||
|
||||
/**
|
||||
* TrustedMiners is a set of miner addresses which will be forcibly used
|
||||
* when making new deals. An empty/nil list disables this feature.
|
||||
*/
|
||||
trustedMiners: this.state.trustedMiners,
|
||||
|
||||
/**
|
||||
* Renew indicates deal-renewal configuration.
|
||||
*/
|
||||
renew: {
|
||||
enabled: this.state.renewEnabled,
|
||||
threshold: this.state.renewThreshold,
|
||||
},
|
||||
|
||||
/**
|
||||
* CountryCodes indicates that new deals should select miners on specific countries.
|
||||
*/
|
||||
countryCodes: [],
|
||||
|
||||
/**
|
||||
* Addr is the wallet address used to store the data in filecoin
|
||||
*/
|
||||
addr: this.state.addr,
|
||||
|
||||
/**
|
||||
* MaxPrice is the maximum price that will be spent to store the data, 0 is no max
|
||||
*/
|
||||
maxPrice: this.state.maxPrice,
|
||||
|
||||
/**
|
||||
*
|
||||
* FastRetrieval indicates that created deals should enable the
|
||||
* fast retrieval feature.
|
||||
*/
|
||||
// fastRetrieval: boolean
|
||||
/**
|
||||
* DealStartOffset indicates how many epochs in the future impose a
|
||||
* deadline to new deals being active on-chain. This value might influence
|
||||
* if miners accept deals, since they should seal fast enough to satisfy
|
||||
* this constraint.
|
||||
*/
|
||||
// dealStartOffset: number
|
||||
},
|
||||
});
|
||||
|
||||
if (Events.hasError(response)) {
|
||||
this.setState({ archiving: false });
|
||||
}
|
||||
|
||||
await Window.delay(5000);
|
||||
alert(
|
||||
"Your storage deal was put in the queue. This can take up to 36 hours, check back later."
|
||||
);
|
||||
|
||||
this.props.onAction({ type: "NAVIGATE", href: "/_/filecoin" });
|
||||
};
|
||||
|
||||
_handleRemove = async (cid) => {
|
||||
this.setState({ loading: true });
|
||||
|
||||
await Actions.removeFromBucket({ bucketName: Constants.textile.dealsBucket, cid });
|
||||
|
||||
let networkViewer;
|
||||
try {
|
||||
const response = await fetch("/api/network");
|
||||
const json = await response.json();
|
||||
networkViewer = json.data;
|
||||
} catch (e) {}
|
||||
|
||||
this.setState({
|
||||
networkViewer,
|
||||
loading: false,
|
||||
});
|
||||
};
|
||||
|
||||
_handleAddTrustedMiner = () => {
|
||||
const miner = prompt("Enter the Miner ID to trust.");
|
||||
|
||||
if (Strings.isEmpty(miner)) {
|
||||
return Events.dispatchMessage({ message: "You must provide a miner ID." });
|
||||
}
|
||||
|
||||
if (this.state.trustedMiners.includes(miner)) {
|
||||
return Events.dispatchMessage({
|
||||
message: `${miner} is already on your list of miners to try.`,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
trustedMiners: [miner, ...this.state.trustedMiners],
|
||||
});
|
||||
};
|
||||
|
||||
_handleAddExcludedMiner = () => {
|
||||
const miner = prompt("Enter the Miner ID to exclude.");
|
||||
|
||||
if (Strings.isEmpty(miner)) {
|
||||
return Events.dispatchMessage({ message: "You must provide a miner ID." });
|
||||
}
|
||||
|
||||
if (this.state.excludedMiners.includes(miner)) {
|
||||
return Events.dispatchMessage({
|
||||
message: `${miner} is already on your list of miners to exclude.`,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
excludedMiners: [miner, ...this.state.excludedMiners],
|
||||
});
|
||||
};
|
||||
|
||||
_handleRemoveTrustedMiner = (minerId) => {
|
||||
this.setState({
|
||||
trustedMiners: this.state.trustedMiners.filter((m) => m !== minerId),
|
||||
});
|
||||
};
|
||||
|
||||
_handleRemoveExcludedMiner = (minerId) => {
|
||||
this.setState({
|
||||
excludedMiners: this.state.excludedMiners.filter((m) => m !== minerId),
|
||||
});
|
||||
};
|
||||
|
||||
_handleChange = (e) => {
|
||||
this.setState({ [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
mounted = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { networkViewer } = this.state;
|
||||
const addressMap = {};
|
||||
const addresses = [];
|
||||
let selected = null;
|
||||
let balance = 0;
|
||||
|
||||
if (networkViewer) {
|
||||
// TODO(jim): restore this when the wallet function is back in Pow.
|
||||
}
|
||||
|
||||
let inFil = 0;
|
||||
if (networkViewer) {
|
||||
const filecoinNumber = new FilecoinNumber(`${this.state.maxPrice}`, "attofil");
|
||||
|
||||
inFil = filecoinNumber.toFil();
|
||||
}
|
||||
|
||||
return (
|
||||
<WebsitePrototypeWrapper
|
||||
title={`${this.props.page.pageTitle} • Slate`}
|
||||
url={`${Constants.hostname}${this.props.page.pathname}`}
|
||||
>
|
||||
<ScenePage>
|
||||
<input
|
||||
css={STYLES_FILE_HIDDEN}
|
||||
multiple
|
||||
type="file"
|
||||
id="file"
|
||||
onChange={this._handleUpload}
|
||||
/>
|
||||
|
||||
<ScenePageHeader title="Make a one-off Filecoin Storage Deal">
|
||||
Upload data and make one-off storage deals in the Filecoin network here. Files must be
|
||||
at least 100MB in size.
|
||||
</ScenePageHeader>
|
||||
|
||||
{this.state.networkViewer ? (
|
||||
<React.Fragment>
|
||||
<System.DescriptionGroup
|
||||
style={{ marginTop: 48, maxWidth: 688 }}
|
||||
label="Storage deal files"
|
||||
description="You can add up to 4GB of files."
|
||||
/>
|
||||
|
||||
<Section
|
||||
style={{ marginTop: 24, maxWidth: 688, minWidth: "auto" }}
|
||||
onAction={this.props.onAction}
|
||||
buttons={[
|
||||
{
|
||||
name: "Add file",
|
||||
multiple: true,
|
||||
type: "file",
|
||||
id: "file",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{this.state.loading ? (
|
||||
<div css={STYLES_SPINNER_CONTAINER}>
|
||||
<LoaderSpinner style={{ height: 32, width: 32 }} />
|
||||
</div>
|
||||
) : (
|
||||
<System.Table
|
||||
data={{
|
||||
columns: [
|
||||
{
|
||||
key: "cid",
|
||||
name: "CID",
|
||||
width: "100%",
|
||||
},
|
||||
],
|
||||
rows: this.state.networkViewer.deal.map((file) => {
|
||||
return {
|
||||
cid: (
|
||||
<div css={STYLES_ROW}>
|
||||
<span css={STYLES_LEFT} target="_blank">
|
||||
{file.cid}
|
||||
</span>
|
||||
<span css={STYLES_RIGHT} onClick={() => this._handleRemove(file.cid)}>
|
||||
<SVG.Dismiss height="16px" />
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<System.DescriptionGroup
|
||||
style={{ marginTop: 64, maxWidth: 688 }}
|
||||
label="Miners"
|
||||
description="Specify miners for our deal maker to try first, and specify miners for our deal maker to ignore."
|
||||
/>
|
||||
|
||||
<Section
|
||||
style={{ marginTop: 24, maxWidth: 688, minWidth: "auto" }}
|
||||
buttons={[
|
||||
{
|
||||
name: "Add miner",
|
||||
onClick: this._handleAddTrustedMiner,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<System.Table
|
||||
data={{
|
||||
columns: [
|
||||
{
|
||||
key: "miner",
|
||||
name: "Miner",
|
||||
width: "100%",
|
||||
},
|
||||
],
|
||||
rows: this.state.trustedMiners.map((miner) => {
|
||||
return {
|
||||
miner: (
|
||||
<div css={STYLES_ROW} key={miner}>
|
||||
<span css={STYLES_LEFT} target="_blank">
|
||||
{miner}
|
||||
</span>
|
||||
<span
|
||||
css={STYLES_RIGHT}
|
||||
onClick={() => this._handleRemoveTrustedMiner(miner)}
|
||||
>
|
||||
<SVG.Dismiss height="16px" />
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
style={{ maxWidth: 688, minWidth: "auto" }}
|
||||
buttons={[
|
||||
{
|
||||
name: "Exclude miner",
|
||||
onClick: this._handleAddExcludedMiner,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<System.Table
|
||||
data={{
|
||||
columns: [
|
||||
{
|
||||
key: "miner",
|
||||
name: "Miner",
|
||||
width: "100%",
|
||||
},
|
||||
],
|
||||
rows: this.state.excludedMiners.map((miner) => {
|
||||
return {
|
||||
miner: (
|
||||
<div css={STYLES_ROW} key={miner}>
|
||||
<span css={STYLES_LEFT} target="_blank">
|
||||
Excluding: {miner}
|
||||
</span>
|
||||
<span
|
||||
css={STYLES_RIGHT}
|
||||
onClick={() => this._handleRemoveExcludedMiner(miner)}
|
||||
>
|
||||
<SVG.Dismiss height="16px" />
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<System.DescriptionGroup
|
||||
style={{ marginTop: 64, maxWidth: 688 }}
|
||||
label="Default Filecoin address"
|
||||
description={`${this.state.addr}`}
|
||||
/>
|
||||
|
||||
<System.Input
|
||||
containerStyle={{ marginTop: 32, maxWidth: 688 }}
|
||||
descriptionStyle={{ maxWidth: 688 }}
|
||||
label="Default Filecoin replication and availability factor"
|
||||
description="How many times should we replicate this deal across your selected miners?"
|
||||
name="repFactor"
|
||||
type="number"
|
||||
value={this.state.repFactor}
|
||||
placeholder="Type in amount of miners"
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
|
||||
<System.Input
|
||||
containerStyle={{ marginTop: 24, maxWidth: 688 }}
|
||||
descriptionStyle={{ maxWidth: 688 }}
|
||||
label="Default Filecoin deal duration"
|
||||
description={`Your deal is set for ${Strings.getDaysFromEpoch(
|
||||
this.state.dealMinDuration
|
||||
)}.`}
|
||||
name="dealMinDuration"
|
||||
type="number"
|
||||
unit="epochs"
|
||||
value={this.state.dealMinDuration}
|
||||
placeholder="Type in epochs (1 epoch = ~30 seconds)"
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
|
||||
<System.Input
|
||||
containerStyle={{ marginTop: 24, maxWidth: 688 }}
|
||||
descriptionStyle={{ maxWidth: 688 }}
|
||||
label="Max Filecoin price"
|
||||
unit="attoFIL"
|
||||
type="number"
|
||||
description={`Set the maximum Filecoin price you're willing to pay. The current price you have set is equivalent to ${inFil} FIL`}
|
||||
name="maxPrice"
|
||||
value={this.state.maxPrice}
|
||||
placeholder="Type in amount of Filecoin (attoFIL)"
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
|
||||
<System.CheckBox
|
||||
style={{ marginTop: 48 }}
|
||||
name="encryption"
|
||||
value={this.state.encryption}
|
||||
onChange={this._handleChange}
|
||||
>
|
||||
Encrypt this storage deal. Accessing the contents will require decryption.
|
||||
</System.CheckBox>
|
||||
|
||||
<System.ButtonPrimary
|
||||
style={{ marginTop: 48 }}
|
||||
onClick={this._handleArchive}
|
||||
loading={this.state.archiving}
|
||||
>
|
||||
Make storage deal
|
||||
</System.ButtonPrimary>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<div css={STYLES_SPINNER_CONTAINER}>
|
||||
<LoaderSpinner style={{ height: 32, width: 32 }} />
|
||||
</div>
|
||||
)}
|
||||
</ScenePage>
|
||||
</WebsitePrototypeWrapper>
|
||||
);
|
||||
}
|
||||
}
|
@ -25,7 +25,15 @@ const createDealsTable = db.schema.createTable("deals", function (table) {
|
||||
table.timestamp("createdAt").notNullable().defaultTo(db.raw("now()"));
|
||||
});
|
||||
|
||||
Promise.all([createDealsTable]);
|
||||
const deleteStorageDealSettings = db.schema.table("users", function (table) {
|
||||
table.dropColumns(
|
||||
"settingsDealsAutoApprove",
|
||||
"allowEncryptedDataStorage",
|
||||
"allowAutomaticDataStorage"
|
||||
);
|
||||
});
|
||||
|
||||
Promise.all([deleteStorageDealSettings]);
|
||||
|
||||
Logging.log(`FINISHED: adjust.js`);
|
||||
Logging.log(` CTRL +C to return to terminal.`);
|
||||
|
@ -131,9 +131,6 @@ const addUserColumns = async () => {
|
||||
// table.string("textileToken", 400).nullable();
|
||||
// table.string("textileThreadID").nullable();
|
||||
// table.string("textileBucketCID").nullable();
|
||||
table.boolean("settingsDealsAutoApprove").notNullable().defaultTo(false);
|
||||
table.boolean("allowAutomaticDataStorage").notNullable().defaultTo(true);
|
||||
table.boolean("allowEncryptedDataStorage").notNullable().defaultTo(true);
|
||||
table.jsonb("onboarding").nullable();
|
||||
});
|
||||
};
|
||||
@ -197,10 +194,7 @@ const migrateUserTable = async () => {
|
||||
name: data.name,
|
||||
body: data.body,
|
||||
photo: data.photo,
|
||||
// textileKey: data.tokens?.api,
|
||||
settingsDealsAutoApprove: data.settings?.settings_deals_auto_approve,
|
||||
allowAutomaticDataStorage: data.settings?.allow_automatic_data_storage,
|
||||
allowEncryptedDataStorage: data.settings?.allow_encrypted_data_storage,
|
||||
textileKey: data.tokens?.api,
|
||||
onboarding:
|
||||
data.onboarding || data.status?.hidePrivacyAlert
|
||||
? { ...data.onboarding, hidePrivacyAlert: data.status?.hidePrivacyAlert }
|
||||
@ -349,9 +343,6 @@ Users
|
||||
'data.photo', -> 'photo' MIGRATED
|
||||
'data.status', -> 'onboarding.hidePrivacyAlert' MIGRATED
|
||||
'data.tokens.api', -> 'textileKey' MIGRATED
|
||||
'data.settings.settings_deals_auto_approve', -> 'settingsDealsAutoApprove' MIGRATED
|
||||
'data.settings.allow_automatic_data_storage', -> 'allowAutomaticDataStorage' MIGRATED
|
||||
'data.settings.allow_encrypted_data_storage', -> 'allowEncryptedDataStorage' MIGRATED
|
||||
'data.onboarding', -> 'onboarding' MIGRATED
|
||||
'data.twitter.username', -> 'twitterUsername' MIGRATED
|
||||
'data.twitter.verified', -> 'twitterVerified' MIGRATED
|
||||
|
@ -43,9 +43,6 @@ const createUsersTable = db.schema.createTable("users", function (table) {
|
||||
table.string("textileToken", 400).nullable();
|
||||
table.string("textileThreadID").nullable();
|
||||
table.string("textileBucketCID").nullable();
|
||||
table.boolean("settingsDealsAutoApprove").notNullable().defaultTo(false);
|
||||
table.boolean("allowAutomaticDataStorage").notNullable().defaultTo(true);
|
||||
table.boolean("allowEncryptedDataStorage").notNullable().defaultTo(true);
|
||||
table.jsonb("onboarding").nullable();
|
||||
table.integer("followerCount").notNullable().defaultTo(0);
|
||||
table.integer("slateCount").notNullable().defaultTo(0);
|
||||
|
@ -1,498 +0,0 @@
|
||||
import configs from "~/knexfile";
|
||||
import knex from "knex";
|
||||
import fs from "fs-extra";
|
||||
import "isomorphic-fetch";
|
||||
|
||||
import * as Logging from "~/common/logging";
|
||||
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";
|
||||
import * as Filecoin from "~/common/filecoin";
|
||||
import * as Serializers from "~/node_common/serializers";
|
||||
|
||||
import { Buckets, PrivateKey } from "@textile/hub";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
const envConfig = configs["development"];
|
||||
const db = knex(envConfig);
|
||||
|
||||
// NOTE(jim): These essentially do the same thing.
|
||||
// 1 GB to be considered to even make a deal.
|
||||
const MINIMUM_BYTES_CONSIDERATION = 104857600 * 10;
|
||||
// 1 GB minimum to make deal.
|
||||
const MINIMUM_BYTES_FOR_STORAGE = 104857600 * 10;
|
||||
const STORAGE_BOT_NAME = "STORAGE WORKER";
|
||||
|
||||
// We don't make new buckets if they have more than 10.
|
||||
const BUCKET_LIMIT = 10;
|
||||
const PRACTICE_RUN = true;
|
||||
const SKIP_NEW_BUCKET_CREATION = false;
|
||||
const STORE_MEANINGFUL_ADDRESS_ONLY_AND_PERFORM_NO_ACTIONS = false;
|
||||
const WRITE_TO_SLATE_STORAGE_DEAL_INDEX = true;
|
||||
|
||||
const TEXTILE_KEY_INFO = {
|
||||
key: Environment.TEXTILE_HUB_KEY,
|
||||
secret: Environment.TEXTILE_HUB_SECRET,
|
||||
};
|
||||
|
||||
Logging.log(`RUNNING: worker-heavy-stones.js`);
|
||||
|
||||
const delay = async (waitMs) => {
|
||||
return await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
};
|
||||
|
||||
const minerMap = {};
|
||||
|
||||
const run = async () => {
|
||||
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) => {
|
||||
minerMap[entity.id] = entity;
|
||||
minerMap[entity.id.replace("t", "f")] = entity;
|
||||
});
|
||||
});
|
||||
|
||||
Logs.taskTimeless(`Fetching every user ...`);
|
||||
const response = await Data.getEveryUser();
|
||||
|
||||
let storageUsers = [];
|
||||
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.allowAutomaticDataStorage) {
|
||||
storageUsers.unshift(user);
|
||||
dealUsers = dealUsers + 1;
|
||||
}
|
||||
|
||||
if (user.allowEncryptedDataStorage) {
|
||||
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.allowEncryptedDataStorage,
|
||||
};
|
||||
let buckets;
|
||||
|
||||
await delay(500);
|
||||
|
||||
try {
|
||||
const token = user.textileKey;
|
||||
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.
|
||||
if (userBytes < MINIMUM_BYTES_CONSIDERATION) {
|
||||
Logs.note(`SKIP: ${user.username}, they only have ${Strings.bytesToSize(userBytes)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
printData.bytes = userBytes;
|
||||
|
||||
const FilecoinSingleton = await Utilities.getFilecoinAPIFromUserToken({
|
||||
user,
|
||||
});
|
||||
const { filecoin } = FilecoinSingleton;
|
||||
let balance = 0;
|
||||
let address = null;
|
||||
|
||||
await delay(500);
|
||||
|
||||
try {
|
||||
const addresses = await filecoin.addresses();
|
||||
addresses.forEach((a) => {
|
||||
balance = a.balance;
|
||||
address = a.address;
|
||||
});
|
||||
} catch (e) {
|
||||
Logs.error(e.message);
|
||||
}
|
||||
|
||||
let storageDeals = [];
|
||||
try {
|
||||
const records = await filecoin.storageDealRecords({
|
||||
ascending: false,
|
||||
includePending: false,
|
||||
includeFinal: true,
|
||||
});
|
||||
|
||||
records.forEach((o) => {
|
||||
storageDeals.push({
|
||||
dealId: o.dealInfo.dealId,
|
||||
rootCid: o.rootCid,
|
||||
proposalCid: o.dealInfo.proposalCid,
|
||||
pieceCid: o.dealInfo.pieceCid,
|
||||
addr: o.address,
|
||||
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.
|
||||
totalCostFIL: Filecoin.formatAsFilecoinConversion(
|
||||
o.dealInfo.pricePerEpoch * o.dealInfo.duration
|
||||
),
|
||||
totalCostAttoFIL: o.dealInfo.pricePerEpoch * o.dealInfo.duration,
|
||||
duration: o.dealInfo.duration,
|
||||
formattedDuration: Strings.getDaysFromEpoch(o.dealInfo.duration),
|
||||
activationEpoch: o.dealInfo.activationEpoch,
|
||||
time: o.time,
|
||||
pending: o.pending,
|
||||
createdAt: Strings.toDateSinceEpoch(o.time),
|
||||
userEncryptsDeals: !!user.allowEncryptedDataStorage,
|
||||
miner: minerMap[o.dealInfo.miner] ? minerMap[o.dealInfo.miner] : { id: o.dealInfo.miner },
|
||||
phase: "MARCH",
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
photo: user.photo,
|
||||
name: user.name,
|
||||
},
|
||||
});
|
||||
});
|
||||
} 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${Filecoin.formatAsFilecoinConversion(balance)} remaining\x1b[0m`
|
||||
);
|
||||
|
||||
// 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} ...`);
|
||||
|
||||
Logging.log(dealToSave);
|
||||
const existing = await db.select("*").from("deals").where(hasDealId(dealToSave.dealId));
|
||||
Logging.log(existing);
|
||||
|
||||
if (existing && !existing.error && existing.length) {
|
||||
Logs.error(`${dealToSave.dealId} is already saved.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
Logs.note(`Inserting ${dealToSave.dealId} ...`);
|
||||
await delay(1000);
|
||||
await db.insert({ data: dealToSave, ownerId: user.id }).into("deals").returning("*");
|
||||
Logs.task(`Inserted ${dealToSave.dealId} !!!`);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE(jim): Exit early for analytics purposes.
|
||||
if (STORE_MEANINGFUL_ADDRESS_ONLY_AND_PERFORM_NO_ACTIONS) {
|
||||
Logs.taskTimeless(`Adding address for: ${user.username}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE(jim): Skip users that are out of funds.
|
||||
if (balance === 0) {
|
||||
Logs.error(`OUT OF FUNDS: ${user.username}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
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;
|
||||
}
|
||||
|
||||
// NOTE(jim): Determine open deals
|
||||
try {
|
||||
const { current, history } = await buckets.archives(keyBucket.key);
|
||||
Logging.log(current);
|
||||
Logging.log(history);
|
||||
} 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 and fits requirements !!!`);
|
||||
key = keyBucket.key;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBucket.name.startsWith("encrypted-data-")) {
|
||||
Logs.note(`bucket found: encrypted-data ${keyBucket.key}`);
|
||||
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;
|
||||
}
|
||||
|
||||
// NOTE(jim): Determine open deals
|
||||
try {
|
||||
const { current, history } = await buckets.archives(keyBucket.key);
|
||||
Logging.log(current);
|
||||
Logging.log(history);
|
||||
} 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 and fits requirements !!!`);
|
||||
key = keyBucket.key;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE(jim): Temporarily prevent more buckets from being created for legacy accounts.
|
||||
if (
|
||||
keyBucket.name === "data" &&
|
||||
!SKIP_NEW_BUCKET_CREATION &&
|
||||
userBuckets.length < BUCKET_LIMIT
|
||||
) {
|
||||
key = null;
|
||||
encrypt = !!user.allowEncryptedDataStorage;
|
||||
|
||||
// NOTE(jim): Create a new bucket
|
||||
const newBucketName = encrypt ? `encrypted-data-${uuid()}` : `open-data-${uuid()}`;
|
||||
|
||||
// NOTE(jim): Get the root key of the bucket
|
||||
let bucketSizeBytes = null;
|
||||
let items;
|
||||
try {
|
||||
const path = await buckets.listPath(keyBucket.key, "/");
|
||||
items = path.item;
|
||||
bucketSizeBytes = path.item.size;
|
||||
} catch (e) {
|
||||
Logs.error(e.message);
|
||||
}
|
||||
|
||||
if (bucketSizeBytes && bucketSizeBytes < MINIMUM_BYTES_FOR_STORAGE) {
|
||||
Logs.error(`Root 'data' bucket does not fit size requirements. Skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await delay(1000);
|
||||
|
||||
Logs.task(`creating new bucket: ${newBucketName}.`);
|
||||
|
||||
// NOTE(jim): Create a new bucket
|
||||
try {
|
||||
Logs.note(`attempting ... `);
|
||||
|
||||
if (!PRACTICE_RUN) {
|
||||
Logs.note(`name: ${newBucketName} ...`);
|
||||
Logs.note(`cid: ${items.cid} ...`);
|
||||
let newBucket = await buckets.create(newBucketName, encrypt, items.cid);
|
||||
|
||||
key = newBucket.root.key;
|
||||
|
||||
Logs.task(`created ${newBucketName} successfully with new key ${key}.`);
|
||||
} else {
|
||||
Logs.note(`practice skipping ...`);
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
Logs.error(e.message);
|
||||
}
|
||||
|
||||
await delay(5000);
|
||||
}
|
||||
|
||||
if (key) {
|
||||
await delay(500);
|
||||
|
||||
try {
|
||||
if (!PRACTICE_RUN) {
|
||||
// NOTE(jim): THE DEAL HAPPENS HERE
|
||||
// DON'T DO IT IF YOU DON'T WANT THE DEAL
|
||||
Logs.task(`KICKING OFF THE DEAL`);
|
||||
await buckets.archive(key, {
|
||||
repFactor: 4,
|
||||
dealMinDuration: 518400,
|
||||
excludedMiners: null,
|
||||
trustedMiners: [
|
||||
// NOTE(jim): ChainSafe
|
||||
// f01247 belongs to ChainSafe, they have white-list rule, you need to ask them add your address
|
||||
"f01247",
|
||||
"f01278",
|
||||
"f071624",
|
||||
"f0135078",
|
||||
"f09848",
|
||||
"f010617",
|
||||
"f01276",
|
||||
"f02401",
|
||||
"f02387",
|
||||
"f019104",
|
||||
"f014409",
|
||||
"f066596",
|
||||
"f058369",
|
||||
"f08399",
|
||||
"f015927",
|
||||
],
|
||||
countryCodes: null,
|
||||
renew: {
|
||||
enabled: false,
|
||||
threshold: 0,
|
||||
},
|
||||
maxPrice: 192901234500,
|
||||
fastRetrieval: true,
|
||||
dealStartOffset: 8640,
|
||||
});
|
||||
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];
|
||||
|
||||
Logs.task(`Show us the history!`);
|
||||
try {
|
||||
const { current, history } = await buckets.archives(targetBucket.key);
|
||||
Logging.log(current);
|
||||
Logging.log(history);
|
||||
} catch (e) {
|
||||
Logs.error(e.message);
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logging.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`);
|
||||
|
||||
Logging.log(`${STORAGE_BOT_NAME} finished. \n\n`);
|
||||
Logging.log(`FINISHED: worker-heavy-stones.js`);
|
||||
Logging.log(` CTRL +C to return to terminal.`);
|
||||
};
|
||||
|
||||
run();
|
@ -195,9 +195,13 @@ const run = async (props) => {
|
||||
.whereRaw('users.id = "files"."ownerId"')
|
||||
.where("files.isLink", false);
|
||||
})
|
||||
.limit(1);
|
||||
.whereNotExists(function () {
|
||||
this.select("id")
|
||||
.from("deals")
|
||||
.whereRaw('"users"."textileBucketCID" = "deals"."textileBucketCID"');
|
||||
});
|
||||
for (let user of users) {
|
||||
if (i % 100 === 0) {
|
||||
if (i % 500 === 0) {
|
||||
console.log(i);
|
||||
if (successful.length) {
|
||||
await DB.insert(successful).into("deals");
|
||||
@ -219,6 +223,7 @@ const run = async (props) => {
|
||||
if (successful.length) {
|
||||
await DB.insert(successful).into("deals");
|
||||
}
|
||||
console.log("SCRIPT FINISHED");
|
||||
};
|
||||
|
||||
const addToEstuary = async (cid) => {
|
||||
|
Loading…
Reference in New Issue
Block a user