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 { css } from "@emotion/react"; import { createState } from "~/scenes/SceneSettings"; import { LoaderSpinner } from "~/components/system/components/Loaders"; import { FilecoinNumber, Converter } from "@openworklabs/filecoin-number"; import { dispatchCustomEvent } from "~/common/custom-events"; import Section from "~/components/core/Section"; import ScenePage from "~/components/core/ScenePage"; import ScenePageHeader from "~/components/core/ScenePageHeader"; import TestnetBanner from "~/components/core/TestnetBanner"; const STAGING_DEAL_BUCKET = "stage-deal"; 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.brand}; } `; 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) {} this.setState({ networkViewer, ...createState(networkViewer.powerInfo.defaultStorageConfig), settings_cold_default_max_price: 1000000000000000, 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: STAGING_DEAL_BUCKET, 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: STAGING_DEAL_BUCKET, 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.settings_cold_default_replication_factor), /** * DealMinDuration indicates the duration to be used when making new deals. */ dealMinDuration: this.state.settings_cold_default_duration, /** * 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.settings_cold_default_excluded_miners, /** * 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.settings_cold_default_trusted_miners, /** * Renew indicates deal-renewal configuration. */ renew: { enabled: this.state.settings_cold_default_auto_renew, threshold: this.state.settings_cold_default_auto_renew_max_price, }, /** * 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.settings_cold_default_address, /** * MaxPrice is the maximum price that will be spent to store the data, 0 is no max */ maxPrice: this.state.settings_cold_default_max_price, /** * * 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 (!response) { this.setState({ archiving: false }); return dispatchCustomEvent({ name: "create-alert", detail: { alert: { message: DEFAULT_ERROR_MESSAGE, }, }, }); } if (response.error) { this.setState({ archiving: false }); if (response.message) { return dispatchCustomEvent({ name: "create-alert", detail: { alert: { message: `From Textile: ${response.message}`, }, }, }); } return dispatchCustomEvent({ name: "create-alert", detail: { alert: { message: Messages.error[response.decorator] ? Messages.error[response.decorator] : DEFAULT_ERROR_MESSAGE, }, }, }); } await Window.delay(5000); alert("The storage deal was made!"); this.props.onAction({ type: "NAVIGATE", value: "V1_NAVIGATION_ARCHIVE" }); }; _handleRemove = async (cid) => { this.setState({ loading: true }); await Actions.removeFromBucket({ bucketName: STAGING_DEAL_BUCKET, 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 dispatchCustomEvent({ name: "create-alert", detail: { alert: { message: "You must provide a miner ID.", }, }, }); } if (this.state.settings_cold_default_trusted_miners.includes(miner)) { return dispatchCustomEvent({ name: "create-alert", detail: { alert: { message: `${miner} is already on your list of miners to try.`, }, }, }); } this.setState({ settings_cold_default_trusted_miners: [ miner, ...this.state.settings_cold_default_trusted_miners, ], }); }; _handleAddExcludedMiner = () => { const miner = prompt("Enter the Miner ID to exclude."); if (Strings.isEmpty(miner)) { return dispatchCustomEvent({ name: "create-alert", detail: { alert: { message: "You must provide a miner ID.", }, }, }); } if (this.state.settings_cold_default_excluded_miners.includes(miner)) { return dispatchCustomEvent({ name: "create-alert", detail: { alert: { message: `${miner} is already on your list of miners to exclude.`, }, }, }); } this.setState({ settings_cold_default_excluded_miners: [ miner, ...this.state.settings_cold_default_excluded_miners, ], }); }; _handleRemoveTrustedMiner = (minerId) => { this.setState({ settings_cold_default_trusted_miners: this.state.settings_cold_default_excluded_miners.filter( (m) => m !== minerId ), }); }; _handleRemoveExcludedMiner = (minerId) => { this.setState({ settings_cold_default_excluded_miners: this.state.settings_cold_default_excluded_miners.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) { networkViewer.powerInfo.balancesList.forEach((a) => { addressMap[a.addr.addr] = { ...a.addr, balance: a.balance }; addresses.push({ ...a.addr, balance: a.balance }); }); if (addresses.length) { selected = addresses[0]; } let transactions = []; if (selected.transactions) { transactions = [...selected.transactions]; } balance = Strings.formatAsFilecoinConversion(selected.balance); } let inFil = 0; if (networkViewer) { const filecoinNumber = new FilecoinNumber( `${this.state.settings_cold_default_max_price}`, "attofil" ); inFil = filecoinNumber.toFil(); } console.log(this.state); return ( <ScenePage> <input css={STYLES_FILE_HIDDEN} multiple type="file" id="file" onChange={this._handleUpload} /> <ScenePageHeader title="Make an one-off Filecoin Storage Deal"> Upload data and make one-off storage deals in the Filecoin network here. </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 style={{ padding: 24 }}> <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.settings_cold_default_trusted_miners.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.settings_cold_default_excluded_miners.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="Configure your deal" description={`Your deal will come out of your wallet address: ${ this.state.settings_cold_default_address }`} /> <System.Input containerStyle={{ marginTop: 48, 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="settings_cold_default_replication_factor" type="number" value={this.state.settings_cold_default_replication_factor} 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.settings_cold_default_duration )}.`} name="settings_cold_default_duration" type="number" unit="epochs" value={this.state.settings_cold_default_duration} 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="settings_cold_default_max_price" value={this.state.settings_cold_default_max_price} 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> ) : ( <LoaderSpinner style={{ marginTop: 48, height: 32, width: 32 }} /> )} </ScenePage> ); } }